From e4218c2894a58f47ab0008ed9f1d2ba7f0127aba Mon Sep 17 00:00:00 2001 From: murtaza98 <murtaza.patrawala@rocket.chat> Date: Fri, 2 Sep 2022 19:59:01 +0530 Subject: [PATCH 001/107] Bump version to 5.2.0-develop --- apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 60144f4ec05..2a403759721 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.1.0 +ENV RC_VERSION 5.2.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 63bad72d9a6..b70e9be373d 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.1.0" + "version": "5.2.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 41f708c4280..e843d12476b 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.1.0", + "version": "5.2.0-develop", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 06948fd1145..86462fe179f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.1.0", + "version": "5.2.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From 50b252b4d26961cb3e99e641ba2ca9667bff4bfe Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Fri, 2 Sep 2022 11:20:56 -0600 Subject: [PATCH 002/107] Chore: Add E2E tests to missing omnichannel endpoints (#26730) --- .../app/livechat/server/api/v1/agent.js | 94 ++++++------ .../app/livechat/server/api/v1/customField.js | 34 ++--- .../meteor/app/livechat/server/api/v1/room.js | 89 ++++++----- .../app/livechat/server/api/v1/transcript.js | 24 ++- .../app/livechat/server/api/v1/visitor.ts | 2 +- .../app/livechat/server/lib/Livechat.js | 5 + apps/meteor/tests/data/livechat/rooms.ts | 2 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 81 ++++++++++ .../end-to-end/api/livechat/01-agents.ts | 49 +++++- .../api/livechat/03-custom-fields.ts | 117 +++++++++++++++ .../end-to-end/api/livechat/09-visitors.ts | 139 +++++++++++++++++- .../end-to-end/api/livechat/11-livechat.ts | 57 +++++++ 12 files changed, 555 insertions(+), 138 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/agent.js b/apps/meteor/app/livechat/server/api/v1/agent.js index 104f0bc012e..1c33d8fbd16 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.js +++ b/apps/meteor/app/livechat/server/api/v1/agent.js @@ -7,72 +7,64 @@ import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { async get() { - try { - check(this.urlParams, { - rid: String, - token: String, - }); + check(this.urlParams, { + rid: String, + token: String, + }); - const visitor = await findGuest(this.urlParams.token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(this.urlParams.token, this.urlParams.rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const visitor = await findGuest(this.urlParams.token); + if (!visitor) { + throw new Meteor.Error('invalid-token'); + } - const agent = room && room.servedBy && findAgent(room.servedBy._id); - if (!agent) { - throw new Meteor.Error('invalid-agent'); - } + const room = findRoom(this.urlParams.token, this.urlParams.rid); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - return API.v1.success({ agent }); - } catch (e) { - return API.v1.failure(e); + const agent = room && room.servedBy && findAgent(room.servedBy._id); + if (!agent) { + throw new Meteor.Error('invalid-agent'); } + + return API.v1.success({ agent }); }, }); API.v1.addRoute('livechat/agent.next/:token', { get() { - try { - check(this.urlParams, { - token: String, - }); + check(this.urlParams, { + token: String, + }); - check(this.queryParams, { - department: Match.Maybe(String), - }); + check(this.queryParams, { + department: Match.Maybe(String), + }); - const { token } = this.urlParams; - const room = findOpenRoom(token); - if (room) { - return API.v1.success(); - } - - let { department } = this.queryParams; - if (!department) { - const requireDeparment = Livechat.getRequiredDepartment(); - if (requireDeparment) { - department = requireDeparment._id; - } - } + const { token } = this.urlParams; + const room = findOpenRoom(token); + if (room) { + return API.v1.success(); + } - const agentData = Promise.await(Livechat.getNextAgent(department)); - if (!agentData) { - throw new Meteor.Error('agent-not-found'); + let { department } = this.queryParams; + if (!department) { + const requireDeparment = Livechat.getRequiredDepartment(); + if (requireDeparment) { + department = requireDeparment._id; } + } - const agent = findAgent(agentData.agentId); - if (!agent) { - throw new Meteor.Error('invalid-agent'); - } + const agentData = Promise.await(Livechat.getNextAgent(department)); + if (!agentData) { + throw new Meteor.Error('agent-not-found'); + } - return API.v1.success({ agent }); - } catch (e) { - return API.v1.failure(e); + const agent = findAgent(agentData.agentId); + if (!agent) { + throw new Meteor.Error('invalid-agent'); } + + return API.v1.success({ agent }); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/customField.js b/apps/meteor/app/livechat/server/api/v1/customField.js index 2c1b7f36912..9fae84e4372 100644 --- a/apps/meteor/app/livechat/server/api/v1/customField.js +++ b/apps/meteor/app/livechat/server/api/v1/customField.js @@ -8,29 +8,25 @@ import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFiel API.v1.addRoute('livechat/custom.field', { async post() { - try { - check(this.bodyParams, { - token: String, - key: String, - value: String, - overwrite: Boolean, - }); - - const { token, key, value, overwrite } = this.bodyParams; + check(this.bodyParams, { + token: String, + key: String, + value: String, + overwrite: Boolean, + }); - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } + const { token, key, value, overwrite } = this.bodyParams; - if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { - return API.v1.failure(); - } + const guest = await findGuest(token); + if (!guest) { + throw new Meteor.Error('invalid-token'); + } - return API.v1.success({ field: { key, value, overwrite } }); - } catch (e) { - return API.v1.failure(e); + if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { + return API.v1.failure(); } + + return API.v1.success({ field: { key, value, overwrite } }); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js index cbbbaaf4db5..47c68afabf8 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ b/apps/meteor/app/livechat/server/api/v1/room.js @@ -234,36 +234,33 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-l-room'] }, { put() { - try { - check(this.bodyParams, { - rid: String, - oldVisitorId: String, - newVisitorId: String, - }); - - const { rid, newVisitorId, oldVisitorId } = this.bodyParams; - - const { visitor } = Promise.await(findVisitorInfo({ userId: this.userId, visitorId: newVisitorId })); - if (!visitor) { - throw new Meteor.Error('invalid-visitor'); - } - - let room = LivechatRooms.findOneById(rid, { _id: 1 }); // TODO: check _id - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const { v: { _id: roomVisitorId } = {} } = room; // TODO: v it will be undefined - if (roomVisitorId !== oldVisitorId) { - throw new Meteor.Error('invalid-room-visitor'); - } - - room = Livechat.changeRoomVisitor(this.userId, rid, visitor); - - return API.v1.success({ room }); - } catch (e) { - return API.v1.failure(e); + // This endpoint is deprecated and will be removed in future versions. + check(this.bodyParams, { + rid: String, + oldVisitorId: String, + newVisitorId: String, + }); + + const { rid, newVisitorId, oldVisitorId } = this.bodyParams; + + const { visitor } = Promise.await(findVisitorInfo({ userId: this.userId, visitorId: newVisitorId })); + if (!visitor) { + throw new Meteor.Error('invalid-visitor'); + } + + let room = LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + if (!room) { + throw new Meteor.Error('invalid-room'); } + + const { v: { _id: roomVisitorId } = {} } = room; // TODO: v it will be undefined + if (roomVisitorId !== oldVisitorId) { + throw new Meteor.Error('invalid-room-visitor'); + } + + room = Livechat.changeRoomVisitor(this.userId, rid, visitor); + + return API.v1.success({ room }); }, }, ); @@ -273,33 +270,29 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-l-room'] }, { get() { - try { - check(this.queryParams, { roomId: String }); + check(this.queryParams, { roomId: String }); - const { roomId } = this.queryParams; + const { roomId } = this.queryParams; - const { user } = this; + const { user } = this; - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); - } + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); + } - const room = LivechatRooms.findOneById(roomId); + const room = LivechatRooms.findOneById(roomId); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); - } + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); + } - if (!canAccessRoom(room, user)) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); + } - addUserToRoom(roomId, user); + addUserToRoom(roomId, user); - return API.v1.success(); - } catch (e) { - return API.v1.failure(e); - } + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.js b/apps/meteor/app/livechat/server/api/v1/transcript.js index 040bb51a0f6..d57b5277a2a 100644 --- a/apps/meteor/app/livechat/server/api/v1/transcript.js +++ b/apps/meteor/app/livechat/server/api/v1/transcript.js @@ -6,21 +6,17 @@ import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/transcript', { async post() { - try { - check(this.bodyParams, { - token: String, - rid: String, - email: String, - }); + check(this.bodyParams, { + token: String, + rid: String, + email: String, + }); - const { token, rid, email } = this.bodyParams; - if (!(await Livechat.sendTranscript({ token, rid, email }))) { - return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') }); - } - - return API.v1.success({ message: TAPi18n.__('Livechat_transcript_sent') }); - } catch (e) { - return API.v1.failure(e); + const { token, rid, email } = this.bodyParams; + if (!(await Livechat.sendTranscript({ token, rid, email }))) { + return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') }); } + + return API.v1.success({ message: TAPi18n.__('Livechat_transcript_sent') }); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 05218621284..470626805af 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -46,7 +46,7 @@ API.v1.addRoute('livechat/visitor', { await Promise.all(rooms.map((room: IRoom) => Livechat.saveRoomInfo(room, visitor))); } - if (customFields && customFields instanceof Array) { + if (customFields && Array.isArray(customFields)) { customFields.forEach((field) => { const customField = Promise.await(LivechatCustomField.findOneById(field.key)); if (!customField) { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 5cd6d846e8f..f07d4610f49 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1178,6 +1178,11 @@ export const Livechat = { const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 }, }); + + if (!visitor) { + throw new Meteor.Error('error-invalid-token', 'Invalid token'); + } + const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en'; const timezone = getTimezone(user); Livechat.logger.debug(`Transcript will be sent using ${timezone} as timezone`); diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index af3fa4dfa9f..6a51e596db9 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -42,7 +42,7 @@ export const createVisitor = (department?: string): Promise<ILivechatVisitor> => }); }); -export const takeInquiry = (roomId: string, _agentId: string): Promise<IOmnichannelRoom> => { +export const takeInquiry = (roomId: string, _agentId?: string): Promise<IOmnichannelRoom> => { return new Promise((resolve, reject) => { request .post(methodCall(`livechat:takeInquiry`)) diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index d1d26f229b9..12b2f0b9935 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -44,6 +44,67 @@ describe('LIVECHAT - rooms', function () { }); }); + describe('livechat/room', () => { + it('should fail when token is not passed as query parameter', async () => { + await request.get(api('livechat/room')).expect(400); + }); + it('should fail when token is not a valid guest token', async () => { + await request.get(api('livechat/room')).query({ token: 'invalid-token' }).expect(400); + }); + it('should fail if rid is passed but doesnt point to a valid room', async () => { + const visitor = await createVisitor(); + await request.get(api('livechat/room')).query({ token: visitor.token, rid: 'invalid-rid' }).expect(400); + }); + it('should create a room for visitor', async () => { + const visitor = await createVisitor(); + const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('room'); + expect(body.room).to.have.property('v'); + expect(body.room.v).to.have.property('token', visitor.token); + expect(body.room.source.type).to.be.equal('api'); + }); + it('should return an existing open room when visitor has one available', async () => { + const visitor = await createVisitor(); + const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('room'); + expect(body.room).to.have.property('v'); + expect(body.room.v).to.have.property('token', visitor.token); + + const { body: body2 } = await request.get(api('livechat/room')).query({ token: visitor.token }); + + expect(body2).to.have.property('success', true); + expect(body2).to.have.property('room'); + expect(body2.room).to.have.property('_id', body.room._id); + expect(body2.newRoom).to.be.false; + }); + it('should return a room for the visitor when rid points to a valid open room', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const { body } = await request.get(api('livechat/room')).query({ token: visitor.token, rid: room._id }); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('room'); + expect(body.room.v).to.have.property('token', visitor.token); + expect(body.newRoom).to.be.false; + }); + it('should properly read widget cookies', async () => { + const visitor = await createVisitor(); + const { body } = await request + .get(api('livechat/room')) + .set('Cookie', [`rc_room_type=l`, `rc_is_widget=t`]) + .query({ token: visitor.token }); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('room'); + expect(body.room.v).to.have.property('token', visitor.token); + expect(body.room.source.type).to.be.equal('widget'); + }); + }); + describe('livechat/rooms', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-rooms', []).then(() => { @@ -185,6 +246,26 @@ describe('LIVECHAT - rooms', function () { }); }); + describe('livechat/room.join', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: '123' }).send().expect(403); + }); + it('should fail if no roomId is present on query params', async () => { + await updatePermission('view-l-room', ['admin', 'livechat-agent']); + await request.get(api('livechat/room.join')).set(credentials).expect(400); + }); + it('should fail if room is present but invalid', async () => { + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: 'invalid' }).send().expect(400); + }); + it('should allow user to join room', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: room._id }).send().expect(200); + }); + }); + describe('livechat/room.close', () => { it('should return an "invalid-token" error when the visitor is not found due to an invalid token', (done) => { request diff --git a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts index 5b7ac465f13..1eb5d483895 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { createAgent, createManager } from '../../../data/livechat/rooms'; +import { createAgent, createManager, createVisitor, createLivechatRoom, takeInquiry, fetchInquiry } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { createUser } from '../../../data/users.helper'; @@ -18,6 +18,7 @@ describe('LIVECHAT - Agents', function () { before((done) => { updateSetting('Livechat_enabled', true) + .then(() => updateSetting('Livechat_Routing_Method', 'Manual_Selection')) .then(createAgent) .then((createdAgent) => { agent = createdAgent; @@ -323,10 +324,54 @@ describe('LIVECHAT - Agents', function () { }); }); }); + + describe('livechat/agent.info/:rid/:token', () => { + it('should fail when token in url params is not valid', async () => { + await request.get(api(`livechat/agent.info/soemthing/invalid-token`)).expect(400); + }); + it('should fail when token is valid but rid isnt', async () => { + const visitor = await createVisitor(); + await request.get(api(`livechat/agent.info/invalid-rid/${visitor.token}`)).expect(400); + }); + /* it('should fail when room is not being served by any agent', async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + await request.get(api(`livechat/agent.info/${room._id}/${visitor.token}`)).expect(400); + }); */ + it('should return a valid agent when the room is being served and the room belongs to visitor', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const inq = await fetchInquiry(room._id); + await takeInquiry(inq._id); + + const { body } = await request.get(api(`livechat/agent.info/${room._id}/${visitor.token}`)); + expect(body).to.have.property('success', true); + expect(body).to.have.property('agent'); + expect(body.agent).to.have.property('_id', 'rocketchat.internal.admin.test'); + }); + }); + describe('livechat/agent.next/:token', () => { + it('should fail when token in url params is not valid', async () => { + await request.get(api(`livechat/agent.next/invalid-token`)).expect(400); + }); + it('should return success when visitor with token has an open room', async () => { + const visitor = await createVisitor(); + await createLivechatRoom(visitor.token); + + await request.get(api(`livechat/agent.next/${visitor.token}`)).expect(200); + }); + it('should fail if theres no open room for visitor and algo is manual selection', async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const visitor = await createVisitor(); + + await request.get(api(`livechat/agent.next/${visitor.token}`)).expect(400); + }); + // TODO: test cases when algo is Auto_Selection + }); }); // TODO: // Missing tests for following endpoint: // livechat/users/:type/:_id -// livechat/agent.info/:rid/:token // livechat/agent.next/:token diff --git a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts index 4d03c150f21..8681d04d88c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts @@ -1,9 +1,12 @@ /* eslint-env mocha */ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { createCustomField } from '../../../data/livechat/custom-fields'; +import { createVisitor } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; describe('LIVECHAT - custom fields', function () { @@ -73,4 +76,118 @@ describe('LIVECHAT - custom fields', function () { }); }); }); + + describe('livechat/custom.field', () => { + it('should fail when token is not on body params', async () => { + await request.post(api('livechat/custom.field')).expect(400); + }); + it('should fail when key is not on body params', async () => { + await request.post(api('livechat/custom.field')).send({ token: 'invalid-token' }).expect(400); + }); + it('should fail when value is not on body params', async () => { + await request.post(api('livechat/custom.field')).send({ token: 'invalid-token', key: 'invalid-key' }).expect(400); + }); + it('should fail when overwrite is not on body params', async () => { + await request + .post(api('livechat/custom.field')) + .send({ token: 'invalid-token', key: 'invalid-key', value: 'invalid-value' }) + .expect(400); + }); + it('should fail when token is invalid', async () => { + await request + .post(api('livechat/custom.field')) + .send({ token: 'invalid-token', key: 'invalid-key', value: 'invalid-value', overwrite: true }) + .expect(400); + }); + it('should fail when key is invalid', async () => { + const visitor = await createVisitor(); + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: 'invalid-key', value: 'invalid-value', overwrite: true }) + .expect(400); + }); + it('should save a custom field on visitor', async () => { + const visitor = await createVisitor(); + const customFieldName = `new_custom_field_${Date.now()}`; + await createCustomField({ + searchable: true, + field: customFieldName, + label: customFieldName, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + } as unknown as ILivechatCustomField & { field: string }); + + const { body } = await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: 'test_address', overwrite: true }) + .expect(200); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('field'); + expect(body.field).to.have.property('value', 'test_address'); + }); + }); + + describe('livechat/custom.fields', () => { + it('should fail when token is not on body params', async () => { + await request.post(api('livechat/custom.fields')).expect(400); + }); + it('should fail if customFields is not on body params', async () => { + await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token' }).expect(400); + }); + it('should fail if customFields is not an array', async () => { + await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: 'invalid-custom-fields' }).expect(400); + }); + it('should fail if customFields is an empty array', async () => { + await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: [] }).expect(400); + }); + it('should fail if customFields is an array with invalid objects', async () => { + await request + .post(api('livechat/custom.fields')) + .send({ token: 'invalid-token', customFields: [{}] }) + .expect(400); + }); + it('should fail if token is not a valid token', async () => { + await request + .post(api('livechat/custom.fields')) + .send({ token: 'invalid-token', customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }] }) + .expect(400); + }); + it('should fail when customFields.key is invalid', async () => { + const visitor = await createVisitor(); + await request + .post(api('livechat/custom.fields')) + .send({ + token: visitor.token, + customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }], + }) + .expect(400); + }); + it('should save a custom field on visitor', async () => { + const visitor = await createVisitor(); + const customFieldName = `new_custom_field_${Date.now()}`; + await createCustomField({ + searchable: true, + field: customFieldName, + label: customFieldName, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + } as unknown as ILivechatCustomField & { field: string }); + + const { body } = await request + .post(api('livechat/custom.fields')) + .send({ token: visitor.token, customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }] }) + .expect(200); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('fields'); + expect(body.fields).to.be.an('array'); + expect(body.fields).to.have.lengthOf(1); + expect(body.fields[0]).to.have.property('value', 'test_address'); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index b0d1d9374ac..88931830315 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -30,6 +30,83 @@ describe('LIVECHAT - visitors', function () { ); }); + describe('livechat/visitor', () => { + it('should fail if no "visitor" key is passed as body parameter', async () => { + const { body } = await request.post(api('livechat/visitor')).send({}); + expect(body).to.have.property('success', false); + }); + it('should fail if visitor.token is not present', async () => { + const { body } = await request.post(api('livechat/visitor')).send({ visitor: {} }); + expect(body).to.have.property('success', false); + }); + it('should create a visitor', async () => { + const { body } = await request.post(api('livechat/visitor')).send({ visitor: { token: 'test' } }); + expect(body).to.have.property('success', true); + expect(body).to.have.property('visitor'); + expect(body.visitor).to.have.property('token', 'test'); + + // Ensure all new visitors are created as online :) + expect(body.visitor).to.have.property('status', 'online'); + }); + it('should create a visitor with provided extra information', async () => { + const token = `${new Date().getTime()}-test`; + const phone = new Date().getTime().toString(); + const { body } = await request.post(api('livechat/visitor')).send({ visitor: { token, phone } }); + expect(body).to.have.property('success', true); + expect(body).to.have.property('visitor'); + expect(body.visitor).to.have.property('token', token); + expect(body.visitor).to.have.property('phone'); + expect(body.visitor.phone[0].phoneNumber).to.equal(phone); + }); + it('should save customFields when passed', async () => { + const customFieldName = `new_custom_field_${Date.now()}`; + const token = `${new Date().getTime()}-test`; + await createCustomField({ + searchable: true, + field: customFieldName, + label: customFieldName, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + } as unknown as ILivechatCustomField & { field: string }); + const { body } = await request.post(api('livechat/visitor')).send({ + visitor: { + token, + customFields: [{ key: customFieldName, value: 'Not a real address :)', overwrite: true }], + }, + }); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('visitor'); + expect(body.visitor).to.have.property('token', token); + expect(body.visitor).to.have.property('livechatData'); + expect(body.visitor.livechatData).to.have.property(customFieldName, 'Not a real address :)'); + }); + it('should update a current visitor when phone is same', async () => { + const token = `${new Date().getTime()}-test`; + const token2 = `${new Date().getTime()}-test2`; + const phone = new Date().getTime().toString(); + const { body } = await request.post(api('livechat/visitor')).send({ visitor: { token, phone } }); + expect(body).to.have.property('success', true); + + const { body: body2 } = await request.post(api('livechat/visitor')).send({ + visitor: { + token: token2, + phone, + }, + }); + + expect(body2).to.have.property('success', true); + expect(body2).to.have.property('visitor'); + + // Same visitor won't update the token + expect(body2.visitor).to.have.property('token', token); + expect(body2.visitor).to.have.property('phone'); + expect(body2.visitor.phone[0].phoneNumber).to.equal(phone); + }); + }); + describe('livechat/visitors.info', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-l-room', []) @@ -650,10 +727,68 @@ describe('LIVECHAT - visitors', function () { expect(res.body.contact).to.be.null; }); }); + // Check if this endpoint is still being used + describe('livechat/room.visitor', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + const res = await request.put(api(`livechat/room.visitor`)).set(credentials).send(); + expect(res.body).to.have.property('success', false); + }); + it('should fail if rid is not on body params', async () => { + await updatePermission('view-l-room', ['admin', 'livechat-agent']); + const res = await request.put(api(`livechat/room.visitor`)).set(credentials).send(); + expect(res.body).to.have.property('success', false); + }); + it('should fail if oldVisitorId is not on body params', async () => { + const res = await request.put(api(`livechat/room.visitor`)).set(credentials).send({ rid: 'GENERAL' }); + expect(res.body).to.have.property('success', false); + }); + it('should fail if newVisitorId is not on body params', async () => { + const res = await request.put(api(`livechat/room.visitor`)).set(credentials).send({ rid: 'GENERAL', oldVisitorId: 'GENERAL' }); + expect(res.body).to.have.property('success', false); + }); + it('should fail if oldVisitorId doesnt point to a valid visitor', async () => { + const res = await request + .put(api(`livechat/room.visitor`)) + .set(credentials) + .send({ rid: 'GENERAL', oldVisitorId: 'GENERAL', newVisitorId: 'GENERAL' }); + expect(res.body).to.have.property('success', false); + }); + it('should fail if rid doesnt point to a valid room', async () => { + const visitor = await createVisitor(); + const res = await request + .put(api(`livechat/room.visitor`)) + .set(credentials) + .send({ rid: 'GENERAL', oldVisitorId: visitor._id, newVisitorId: visitor._id }); + expect(res.body).to.have.property('success', false); + }); + it('should fail if oldVisitorId is trying to change a room is not theirs', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const visitor2 = await createVisitor(); + + const res = await request + .put(api(`livechat/room.visitor`)) + .set(credentials) + .send({ rid: room._id, oldVisitorId: visitor2._id, newVisitorId: visitor._id }); + expect(res.body).to.have.property('success', false); + }); + it('should successfully change a room visitor with a new one', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const visitor2 = await createVisitor(); + + const res = await request + .put(api(`livechat/room.visitor`)) + .set(credentials) + .send({ rid: room._id, oldVisitorId: visitor._id, newVisitorId: visitor2._id }); + expect(res.body).to.have.property('success', true); + expect(res.body.room).to.have.property('v'); + expect(res.body.room.v._id).to.equal(visitor2._id); + }); + }); }); // TODO: Missing tests for the following endpoints: // - /v1/livechat/visitor.status // - /v1/livechat/visitor.callStatus -// - /v1/livechat/visitor/:token/room -// - /v1/livechat/visitor diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index 6ead513e370..aab986d32d5 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -129,4 +129,61 @@ describe('LIVECHAT - Utils', function () { expect(body.page).to.have.property('msg'); }); }); + describe('livechat/transcript', () => { + it('should fail if token is not in body params', async () => { + const { body } = await request.post(api('livechat/transcript')).set(credentials).send({}); + expect(body).to.have.property('success', false); + }); + it('should fail if rid is not in body params', async () => { + const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should fail if email is not in body params', async () => { + const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test', rid: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should fail if token is not a valid guest token', async () => { + const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test', rid: 'test', email: '' }); + expect(body).to.have.property('success', false); + }); + it('should fail if rid is not a valid room id', async () => { + const visitor = await createVisitor(); + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor.token, rid: 'test', email: '' }); + expect(body).to.have.property('success', false); + }); + it('should fail if requesting a transcript of another visitors room', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const visitor2 = await createVisitor(); + + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor2.token, rid: room._id, email: '' }); + expect(body).to.have.property('success', false); + }); + it('should fail if email is not a valid email', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor.token, rid: room._id, email: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should send a transcript if all is good', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' }); + expect(body).to.have.property('success', true); + }); + }); }); -- GitLab From 839e3860737e16f3c98bbb8868414b3b810db9d7 Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:55:59 +0000 Subject: [PATCH 003/107] =?UTF-8?q?i18n:=20Language=20update=20from=20Ling?= =?UTF-8?q?oHub=20=F0=9F=A4=96=20on=202022-08-29Z=20(#26722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/eu.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json | 4 ++-- 57 files changed, 107 insertions(+), 106 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json index a0e5fc25f48..d08cbd123e1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json @@ -1643,6 +1643,7 @@ "manage-assets_description": "Toestemming om die bedienerbates te bestuur", "manage-emoji": "Bestuur Emoji", "manage-emoji_description": "Toestemming om die bediener emojis te bestuur", + "messages_pruned": "boodskappe snoei", "manage-integrations": "Bestuur integrasies", "manage-integrations_description": "Toestemming om die bedienerintegrasies te bestuur", "manage-oauth-apps": "Bestuur Oauth Apps", @@ -1754,7 +1755,6 @@ "Message_view_mode_info": "Dit verander die hoeveelheid spasie boodskappe wat op die skerm opgeneem word.", "messages": "boodskappe", "Messages": "boodskappe", - "messages_pruned": "boodskappe snoei", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Boodskappe wat na die Inkomende WebHook gestuur word, sal hier verskyn.", "Meta": "meta", "Meta_custom": "Aangepaste Meta Tags", @@ -2795,4 +2795,4 @@ "Your_push_was_sent_to_s_devices": "Jou druk is gestuur na%s toestelle", "Your_server_link": "Jou bediener skakel", "Your_workspace_is_ready": "Jou werkruimte is gereed om 🎉 te gebruik" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index fb566b34f82..4091d89128e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -2823,6 +2823,7 @@ "manage-email-inbox_description": "إذن لإدارة صناديق البريد الإلكتروني", "manage-emoji": "إدارة الرموز التعبيرية", "manage-emoji_description": "إذن لإدارة الرموز التعبيرية للخادم", + "messages_pruned": "تم ØªÙ†Ù‚ÙŠØ Ø§Ù„Ø±Ø³Ø§Ø¦Ù„", "manage-incoming-integrations": "إدارة عمليات التكامل الواردة", "manage-incoming-integrations_description": "إذن لإدارة عمليات تكامل الخادم الواردة", "manage-integrations": "إدارة عمليات التكامل", @@ -3028,7 +3029,6 @@ "MessageBox_view_mode": "وضع عرض MessageBox", "messages": "الرسائل", "Messages": "الرسائل", - "messages_pruned": "تم ØªÙ†Ù‚ÙŠØ Ø§Ù„Ø±Ø³Ø§Ø¦Ù„", "Messages_sent": "تم إرسال الرسائل", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "سيتم نشر الرسائل التي تم إرسالها إلى خطا٠الويب الوارد هنا.", "Meta": "التعريÙ", @@ -4976,4 +4976,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "لن تكون بعض الخدمات متاØØ© أو ستتطلب إعدادًا يدويًا", "onboarding.form.standaloneServerForm.publishOwnApp": "لإرسال الإشعارات، تØتاج إلى تجميع تطبيقك الخاص ونشره على Google Play ÙˆApp Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "تØتاج إلى التكامل مع الخدمات الخارجية يدويًا" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json index 9f9cd241161..102732abf51 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json @@ -1643,6 +1643,7 @@ "manage-assets_description": "Server aktivlÉ™rini idarÉ™ etmÉ™k üçün icazÉ™", "manage-emoji": "Emoji idarÉ™ et", "manage-emoji_description": "Server emojisinin idarÉ™ olunması üçün icazÉ™", + "messages_pruned": "mesajlar buddur", "manage-integrations": "BütünlüklÉ™ri idarÉ™ et", "manage-integrations_description": "Server inteqrasiyasını idarÉ™ etmÉ™k üçün icazÉ™", "manage-oauth-apps": "Oauth Apps idarÉ™ et", @@ -1754,7 +1755,6 @@ "Message_view_mode_info": "Bu, yer mesajlarının ekranda çəkilmÉ™sini azaldır.", "messages": "Mesajlar", "Messages": "Mesajlar", - "messages_pruned": "mesajlar buddur", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Daxil olan WebHook-a göndÉ™rilÉ™n mesajlar burada yerləşdirilÉ™cÉ™k.", "Meta": "Meta", "Meta_custom": "Xüsusi Meta Tags", @@ -2795,4 +2795,4 @@ "Your_push_was_sent_to_s_devices": "Sizin itÉ™niz%s cihazlarına göndÉ™rildi", "Your_server_link": "Sizin server baÄŸlantınız", "Your_workspace_is_ready": "Ä°ÅŸ yeriniz 🎉 istifadÉ™ etmÉ™yÉ™ hazırdır" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json index aa28c380594..0e1ff0ba748 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json @@ -1659,6 +1659,7 @@ "manage-assets_description": "Дазвол на кіраванне актывамі Ñервераў", "manage-emoji": "кіраванне Emoji", "manage-emoji_description": "Дазвол на кіраванне Ñмайлікамі Ñервера", + "messages_pruned": "Ð¿Ð°Ð²ÐµÐ´Ð°Ð¼Ð»ÐµÐ½Ð½Ñ Ð°Ð±Ñ€Ñзкі", "manage-integrations": "кіраванне інтÑграцый", "manage-integrations_description": "Дазвол на кіраванне інтÑграцыÑй Ñервера", "manage-oauth-apps": "Кіраванне OAuth Apps", @@ -1770,7 +1771,6 @@ "Message_view_mode_info": "ГÑта змÑнÑе колькаÑць каÑмічных паведамленнÑÑž займае на Ñкране.", "messages": "паведамленнÑ", "Messages": "паведамленнÑ", - "messages_pruned": "Ð¿Ð°Ð²ÐµÐ´Ð°Ð¼Ð»ÐµÐ½Ð½Ñ Ð°Ð±Ñ€Ñзкі", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Паведамленні, Ð°Ð´Ð¿Ñ€Ð°ÑžÐ»ÐµÐ½Ñ‹Ñ Ð½Ð° Ð£Ð²Ð°Ñ…Ð¾Ð´Ð½Ð°Ñ WebHook будуць размешчаны тут.", "Meta": "Meta", "Meta_custom": "ПрыÑтаÑÐ°Ð²Ð°Ð½Ñ‹Ñ Ð¼ÐµÑ‚Ð°-Ñ‚Ñгі", @@ -2813,4 +2813,4 @@ "Your_push_was_sent_to_s_devices": "Ваш штуршок быў адпраўлены Ñž%s прылад", "Your_server_link": "Ваша ÑпаÑылка Ñервера", "Your_workspace_is_ready": "Ваша Ð¿Ñ€Ð°Ñ†Ð¾ÑžÐ½Ð°Ñ Ð²Ð¾Ð±Ð»Ð°Ñць гатова да выкарыÑÑ‚Ð°Ð½Ð½Ñ ðŸŽ‰" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json index 7c7e4dfcd26..f03fb92e3c2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json @@ -1640,6 +1640,7 @@ "manage-assets_description": "Разрешение за управление на активите на Ñървъра", "manage-emoji": "Управление на Еможи", "manage-emoji_description": "Разрешение за управление на Ñървърната емоциÑ", + "messages_pruned": "ÑъобщениÑта Ñа подрÑзани", "manage-integrations": "Управление на интеграциите", "manage-integrations_description": "Разрешение за управление на интеграциÑта на Ñървъра", "manage-oauth-apps": "Управление на Oauth Apps", @@ -1751,7 +1752,6 @@ "Message_view_mode_info": "Това Ð¿Ñ€Ð¾Ð¼ÐµÐ½Ñ Ð±Ñ€Ð¾Ñ Ð½Ð° коÑмичеÑките ÑъобщениÑ, които Ñе заемат на екрана.", "messages": "СъобщениÑ", "Messages": "СъобщениÑ", - "messages_pruned": "ÑъобщениÑта Ñа подрÑзани", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "СъобщениÑта, които Ñе изпращат в Incoming WebHook, ще бъдат публикувани тук.", "Meta": "Мета", "Meta_custom": "ПерÑонализирани мета тагове", @@ -2790,4 +2790,4 @@ "Your_push_was_sent_to_s_devices": "ÐатиÑкането ви бе изпратено на %s уÑтройÑтва", "Your_server_link": "Вашата Ñървърна връзка", "Your_workspace_is_ready": "Работното ви проÑтранÑтво е готово за използване 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json index e39f038a230..2788d5a0020 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json @@ -1637,6 +1637,7 @@ "manage-assets_description": "Dozvola za upravljanje imovinom poslužitelja", "manage-emoji": "Upravljanje emojima", "manage-emoji_description": "Dozvola za upravljanje serverom emojis", + "messages_pruned": "poruke su obrezane", "manage-integrations": "Upravljanje integracijama", "manage-integrations_description": "Dozvola za upravljanje integracijama poslužitelja", "manage-oauth-apps": "Upravljanje aplikacijama Oauth", @@ -1748,7 +1749,6 @@ "Message_view_mode_info": "Ovo mijenja koliÄinu prostora koju poruka zauzima na ekranu.", "messages": "Poruke", "Messages": "Poruke", - "messages_pruned": "poruke su obrezane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Poruke koje se Å¡alju na dolazeći WebHook bit će objavljene ovdje.", "Meta": "Meta", "Meta_custom": "PrilagoÄ‘ene meta oznake", @@ -2787,4 +2787,4 @@ "Your_push_was_sent_to_s_devices": "Push obavijest je poslana %s ureÄ‘aje", "Your_server_link": "Veza poslužitelja", "Your_workspace_is_ready": "Radni je prostor spreman za upotrebu 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index a9f15b980a3..7c95d9bcb53 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -2796,6 +2796,7 @@ "manage-email-inbox_description": "PermÃs per administrar safates d'entrada de correu electrònic", "manage-emoji": "Gestionar emoticones", "manage-emoji_description": "PermÃs per gestionar les emoticones del servidor", + "messages_pruned": "missatge esborrat", "manage-incoming-integrations": "Gestioneu les integracions entrants", "manage-incoming-integrations_description": "PermÃs per gestionar les integracions entrants del servidor", "manage-integrations": "Gestionar les integracions", @@ -2999,7 +3000,6 @@ "MessageBox_view_mode": "Mode de visualització de el panell de missatges", "messages": "Missatges", "Messages": "Missatges", - "messages_pruned": "missatge esborrat", "Messages_sent": "Missatges enviats", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Els missatges enviats al WebHook d'entrada seran publicats aquÃ.", "Meta": "Meta", @@ -4784,4 +4784,4 @@ "Your_temporary_password_is_password": "La vostra contrasenya temporal és <strong> [contrasenya] </strong>.", "Your_TOTP_has_been_reset": "El vostre TOTP de dos factors s'ha restablert.", "Your_workspace_is_ready": "El vostre espai de treball està a punt per utilitzar 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index 0a07b1fcb66..c413ef5bf0e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -2350,6 +2350,7 @@ "manage-cloud_description": "Správa Cloudu", "manage-emoji": "Spravovat emotikony", "manage-emoji_description": "Právo mÄ›nit emotikony", + "messages_pruned": "zprávy byly proÄiÅ¡tÄ›ny", "manage-incoming-integrations": "Spravovat pÅ™Ãchozà integrace", "manage-incoming-integrations_description": "OprávnÄ›nà pro správu pÅ™ÃchozÃch serverových integracÃ", "manage-integrations": "Spravovat integrace", @@ -2518,7 +2519,6 @@ "MessageBox_view_mode": "Režim zobrazenà MessageBoxu", "messages": "zprávy", "Messages": "Zprávy", - "messages_pruned": "zprávy byly proÄiÅ¡tÄ›ny", "Messages_sent": "Zpráva odeslána", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Zprávy, které jsou odesÃlány do pÅ™Ãchozà WebHook integrace budou zveÅ™ejnÄ›ny zde.", "Meta": "Meta informace", @@ -4019,4 +4019,4 @@ "Your_server_link": "Odkaz na Váš server", "Your_temporary_password_is_password": "VaÅ¡e doÄasné heslo je <strong>[password]</strong>.", "Your_workspace_is_ready": "Váš prostÅ™edà je pÅ™ipraveno k použità 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json index 4fc1d15a534..34387a0ba1e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json @@ -1638,6 +1638,7 @@ "manage-assets_description": "Caniatâd i reoli asedau'r gweinyddwr", "manage-emoji": "Rheoli Emoji", "manage-emoji_description": "Caniatâd i reoli'r emojis gweinyddwr", + "messages_pruned": "negeseuon pruned", "manage-integrations": "Rheoli Integreiddio", "manage-integrations_description": "Caniatâd i reoli'r integreiddio gweinyddwr", "manage-oauth-apps": "Rheoli Apps Oauth", @@ -1749,7 +1750,6 @@ "Message_view_mode_info": "Mae hyn yn newid faint o negeseuon gofod sy'n eu cymryd ar y sgrin.", "messages": "Negeseuon", "Messages": "Negeseuon", - "messages_pruned": "negeseuon pruned", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Bydd negeseuon sy'n cael eu hanfon at y WebHook sy'n dod i mewn yn cael eu postio yma.", "Meta": "Meta", "Meta_custom": "Tags Meta Custom", @@ -2789,4 +2789,4 @@ "Your_push_was_sent_to_s_devices": "Anfonwyd eich gwthio i ddyfeisiau %s", "Your_server_link": "Dolen eich gweinydd", "Your_workspace_is_ready": "Mae'ch gweithle yn barod i ddefnyddio 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index bb51b19e5e7..cf996168b6b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -2362,6 +2362,7 @@ "manage-cloud_description": "Administrer Cloud", "manage-emoji": "Administrer emojier", "manage-emoji_description": "Tilladelse til at styre server-emojis", + "messages_pruned": "beskeder beskÃ¥ret", "manage-incoming-integrations": "Administrer indgÃ¥ende integrationer", "manage-incoming-integrations_description": "Tilladelse til at administrere serverens indgÃ¥ende integrationer", "manage-integrations": "Administrer integrationer", @@ -2531,7 +2532,6 @@ "MessageBox_view_mode": "MessageBox-visningstilstand", "messages": "Meddelelser", "Messages": "Meddelelser", - "messages_pruned": "beskeder beskÃ¥ret", "Messages_sent": "Besked sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meddelelser, der sendes til den indkommende webhook, bliver sendt her.", "Meta": "Meta", @@ -4043,4 +4043,4 @@ "Your_server_link": "Din server link", "Your_temporary_password_is_password": "Din midlertidige adgangskode er <strong>[password]</strong>.", "Your_workspace_is_ready": "Dit arbejdsomrÃ¥de er klar til brug 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 2817efa10fa..c981c8fd42a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -1645,6 +1645,7 @@ "manage-assets_description": "Berechtigung zum Verwalten der Server-Assets", "manage-emoji": "Verwalten Sie Emoji", "manage-emoji_description": "Berechtigung zum Verwalten der Server-Emojis", + "messages_pruned": "Nachrichten beschnitten", "manage-integrations": "Integrationen verwalten", "manage-integrations_description": "Berechtigung zur Verwaltung der Serverintegrationen", "manage-oauth-apps": "Oauth Apps verwalten", @@ -1756,7 +1757,6 @@ "Message_view_mode_info": "Dadurch ändert sich der Platzbedarf für Nachrichten auf dem Bildschirm.", "messages": "Nachrichten", "Messages": "Nachrichten", - "messages_pruned": "Nachrichten beschnitten", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht.", "Meta": "Metadaten", "Meta_custom": "Benutzerdefinierte Meta-Tags", @@ -2796,4 +2796,4 @@ "Your_push_was_sent_to_s_devices": "Die Push-Nachricht wurde an %s Geräte gesendet.", "Your_server_link": "Ihre Serververbindung", "Your_workspace_is_ready": "Ihr Arbeitsbereich ist einsatzbereit 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json index def3bba02ce..885aa6827d2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json @@ -3213,4 +3213,4 @@ "Your_question": "Deine Frage", "Your_server_link": "Dein Server-Link", "Your_workspace_is_ready": "Dein Arbeitsbereich ist einsatzbereit 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 85274429901..b99451a2a23 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -2956,6 +2956,7 @@ "manage-email-inbox_description": "Berechtigung zur Verwaltung von E-Mail-Postfächern", "manage-emoji": "Emojis verwalten", "manage-emoji_description": "Berechtigung, Emojis zu verwalten", + "messages_pruned": "Nachrichten gelöscht", "manage-incoming-integrations": "Eingehende Integrationen verwalten", "manage-incoming-integrations_description": "Berechtigung zum Verwalten der eingehenden Serverintegrationen", "manage-integrations": "Integrationen verwalten", @@ -3167,7 +3168,6 @@ "MessageBox_view_mode": "MessageBox-Ansichtsmodus", "messages": "Nachrichten", "Messages": "Nachrichten", - "messages_pruned": "Nachrichten gelöscht", "Messages_selected": "Ausgewählte Nachrichten", "Messages_sent": "Nachrichten versandt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht", @@ -5248,4 +5248,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Anmeldung erkannt", "Device_Management_Email_Body": "Sie können die folgenden Platzhalter verwenden: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", "Something_Went_Wrong": "Etwas ist schief gelaufen" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json index acc066668a7..0183f1e36ca 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json @@ -1650,6 +1650,7 @@ "manage-assets_description": "Άδεια διαχείÏισης των πόÏων του διακομιστή", "manage-emoji": "ΔιαχειÏιστείτε το Emoji", "manage-emoji_description": "Άδεια διαχείÏισης του emojis του διακομιστή", + "messages_pruned": "μηνÏματα κλαδÎματος", "manage-integrations": "ΔιαχείÏιση ενοποιήσεων", "manage-integrations_description": "Άδεια διαχείÏισης των ενσωματωμÎνων διακομιστών", "manage-oauth-apps": "ΔιαχείÏιση εφαÏμογών Oauth", @@ -1762,7 +1763,6 @@ "Message_view_mode_info": "Αυτό αλλάζει το ποσό των μηνυμάτων χώÏο καταλαμβάνουν στην οθόνη.", "messages": "μηνÏματα", "Messages": "μηνÏματα", - "messages_pruned": "μηνÏματα κλαδÎματος", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Τα μηνÏματα που αποστÎλλονται στο Incoming WebHook θα αναÏτηθοÏν εδώ.", "Meta": "Meta", "Meta_custom": "Custom Meta Tags", @@ -2802,4 +2802,4 @@ "Your_push_was_sent_to_s_devices": "ώθηση σας στάλθηκε σε συσκευÎÏ‚ %s", "Your_server_link": "ΣÏνδεσμος διακομιστή σας", "Your_workspace_is_ready": "Ο χώÏος εÏγασίας σας είναι Îτοιμος για χÏήση 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index c7efa0695b0..8dfae678d7f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3010,6 +3010,7 @@ "manage-email-inbox_description": "Permission to manage email inboxes", "manage-emoji": "Manage Emoji", "manage-emoji_description": "Permission to manage the server emojis", + "messages_pruned": "messages pruned", "manage-incoming-integrations": "Manage Incoming Integrations", "manage-incoming-integrations_description": "Permission to manage the server incoming integrations", "manage-integrations": "Manage Integrations", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json index 7d74bf46254..92bdccc1970 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json @@ -1643,6 +1643,7 @@ "manage-assets_description": "Permeso por administri la servajn aktivojn", "manage-emoji": "Administri Emoji", "manage-emoji_description": "Permeso por administri la servilon emojis", + "messages_pruned": "mesaÄoj pruntitaj", "manage-integrations": "Administri Integraĵojn", "manage-integrations_description": "Permeso por administri la servadajn integraĵojn", "manage-oauth-apps": "Administri Oauth-Apps", @@ -1754,7 +1755,6 @@ "Message_view_mode_info": "Ĉi tio ÅanÄas la kvanton de spacaj mesaÄoj en la ekrano.", "messages": "MesaÄoj", "Messages": "MesaÄoj", - "messages_pruned": "mesaÄoj pruntitaj", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "MesaÄoj senditaj al la venonta TTT-ejo estos poÅtitaj ĉi tie.", "Meta": "Metas", "Meta_custom": "Propra Meta Etikedoj", @@ -2796,4 +2796,4 @@ "Your_server_link": "Via servilo-ligilo", "Your_temporary_password_is_password": "Via provizora pasvorto estas: <strong>[pasvorto]</strong>", "Your_workspace_is_ready": "Via labora spaco pretas uzi 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 04d6b3d598f..47519db384c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -2791,6 +2791,7 @@ "manage-email-inbox_description": "Permiso para gestionar bandejas de entrada de correo electrónico", "manage-emoji": "Gestionar emojis", "manage-emoji_description": "Permiso para gestionar los emojis del servidor", + "messages_pruned": "mensajes retirados", "manage-incoming-integrations": "Gestionar integraciones entrantes", "manage-incoming-integrations_description": "Permiso para gestionar las integraciones entrantes del servidor", "manage-integrations": "Gestionar integraciones", @@ -2994,7 +2995,6 @@ "MessageBox_view_mode": "Modo de visualización del cuadro de mensaje", "messages": "mensajes", "Messages": "Mensajes", - "messages_pruned": "mensajes retirados", "Messages_sent": "Mensajes enviados", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Los mensajes enviados al webhook entrante se publicarán aquÃ.", "Meta": "Meta", @@ -4921,4 +4921,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Algunos servicios no estarán disponibles o requerirán configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviarte notificaciones push, debes compilar y publicar tu propia aplicación en Google Play y App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Necesita integrarse manualmente con servicios externos" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/eu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/eu.i18n.json index 6aa98755772..f09e55f7519 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/eu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/eu.i18n.json @@ -119,4 +119,4 @@ "Users": "Erabiltzaileak", "We_are_offline_Sorry_for_the_inconvenience": "Lineaz kanpo gaude. Barkatu eragozpenak.", "Yes": "Bai" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json index 6a03d3fcb0e..40d973954d9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -1916,6 +1916,7 @@ "manage-assets_description": "مجوز مدیریت دارایی های سرور", "manage-emoji": "مدیریت Emoji", "manage-emoji_description": "مجوز مدیریت emojis سرور", + "messages_pruned": "پیام ها زده شده اند", "manage-integrations": "مدیریت ادغام", "manage-integrations_description": "مجوز مدیریت ادغام سرور", "manage-livechat-agents": "مدیریت پشتیبان‌های کانال همه‌کاره", @@ -2036,7 +2037,6 @@ "Message_view_mode_info": "این تغییر مقدار از پیام های Ùضایی را بر روی صÙØÙ‡ نمایش.", "messages": "پیام ها", "Messages": "پیام ها", - "messages_pruned": "پیام ها زده شده اند", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "پیام هایی Ú©Ù‡ به ورودی WebHook ارسال خواهد شد در اینجا نوشته شده است.", "Meta": "متا", "Meta_custom": "سÙارشی متا تگ", @@ -3137,4 +3137,4 @@ "Your_push_was_sent_to_s_devices": "Ùشار خود را به دستگاه %s را ارسال شد", "Your_server_link": "لینک سرور شما", "Your_workspace_is_ready": "Ùضای کاری شما آماده استÙاده است" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index ac646dfb585..dfd27626755 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -1643,6 +1643,7 @@ "manage-assets_description": "Käyttöoikeus hallita palvelimen varoja", "manage-emoji": "Hallitse emojia", "manage-emoji_description": "Käyttöoikeus hallita emojis-palvelinta", + "messages_pruned": "viestit piilotettuina", "manage-integrations": "Hallinnoi integraatiota", "manage-integrations_description": "Käyttöoikeus hallita palvelinten integrointeja", "manage-oauth-apps": "Hallitse Oauth-sovelluksia", @@ -1755,7 +1756,6 @@ "Message_view_mode_info": "Tämä muuttaa sitä, kuinka paljon tilaa viestit vievät ruudulla.", "messages": "Viestit", "Messages": "Viestit", - "messages_pruned": "viestit piilotettuina", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Viestit, jotka lähetetään sisääntulevaan WebHookiin, postataan tänne", "Meta": "Meta", "Meta_custom": "Mukautetut Meta Tags", @@ -2794,4 +2794,4 @@ "Your_push_was_sent_to_s_devices": "Push-viestisi lähetettiin %s laitteeseen", "Your_server_link": "Palvelimesi linkki", "Your_workspace_is_ready": "Työtila on valmis käyttämään 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 79ac0a4a2fe..052d0226be7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -2800,6 +2800,7 @@ "manage-email-inbox_description": "Autorisation de gérer les boîtes de réception", "manage-emoji": "Gérer les emojis", "manage-emoji_description": "Autorisation de gérer les emojis du serveur", + "messages_pruned": "messages élagués", "manage-incoming-integrations": "Gérer les intégrations entrantes", "manage-incoming-integrations_description": "Autorisation de gérer les intégrations entrantes du serveur", "manage-integrations": "Gérer les intégrations", @@ -3004,7 +3005,6 @@ "MessageBox_view_mode": "Mode d'affichage MessageBox", "messages": "messages", "Messages": "Messages", - "messages_pruned": "messages élagués", "Messages_sent": "Messages envoyés", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Les messages envoyés au webhook Entrant seront publiés ici.", "Meta": "Meta", @@ -4948,4 +4948,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Certains services ne seront pas disponibles ou nécessiteront une configuration manuelle", "onboarding.form.standaloneServerForm.publishOwnApp": "Pour envoyer des notifications push, vous devez compiler et publier votre propre application sur Google Play et App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Intégration manuelle des services externes requise" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json index 4e95c2a4848..779f3de5995 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json @@ -1545,4 +1545,4 @@ "Your_password_is_wrong": "הסיסמה שלך שגויה!", "Your_push_was_sent_to_s_devices": "הודעת ×”-push × ×©×œ×— בהצלחה ל-%s מכשירי×", "Your_question": "הש×לה שלך" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json index fd46c740e31..98965109b1d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -1775,6 +1775,7 @@ "manage-assets_description": "Dozvola za upravljanje imovinom poslužitelja", "manage-emoji": "Upravljanje emojima", "manage-emoji_description": "Dozvola za upravljanje serverom emojis", + "messages_pruned": "poruke su obrezane", "manage-integrations": "Upravljanje integracijama", "manage-integrations_description": "Dozvola za upravljanje integracijama poslužitelja", "manage-oauth-apps": "Upravljanje aplikacijama Oauth", @@ -1887,7 +1888,6 @@ "Message_view_mode_info": "Ovo mijenja koliÄinu prostora koju poruka zauzima na ekranu.", "messages": "Poruke", "Messages": "Poruke", - "messages_pruned": "poruke su obrezane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Poruke koje se Å¡alju na dolazeći WebHook bit će objavljene ovdje.", "Meta": "Meta", "Meta_custom": "PrilagoÄ‘ene meta oznake", @@ -2930,4 +2930,4 @@ "Your_push_was_sent_to_s_devices": "Push obavijest je poslana %s ureÄ‘aje", "Your_server_link": "Veza poslužitelja", "Your_workspace_is_ready": "Radni je prostor spreman za upotrebu 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 16a2453d355..87476b6e0d5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -2306,6 +2306,7 @@ "manage-cloud_description": "FelhÅ‘ kezelése", "manage-emoji": "Emoji kezelése", "manage-emoji_description": "Engedély a kiszolgáló emojis kezeléséhez", + "messages_pruned": "üzenetek metszenek", "manage-integrations": "Integrációk kezelése", "manage-integrations_description": "Engedély a szerver integrációjának kezelésére", "manage-oauth-apps": "Oauth Apps kezelése", @@ -2448,7 +2449,6 @@ "Message_view_mode_info": "Ez megváltoztatja a tárterületet üzeneteket veszi fel a képernyÅ‘n.", "messages": "Ãœzenetek", "Messages": "Ãœzenetek", - "messages_pruned": "üzenetek metszenek", "Messages_sent": "Ãœzenetek elküldve", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Küldött üzenetek a BejövÅ‘ WebHook felteszik itt.", "Meta": "meta", @@ -3840,4 +3840,4 @@ "Your_temporary_password_is_password": "Az ideiglenes jelszavad: <strong>[password]</strong>", "Your_TOTP_has_been_reset": "A kétfaktoros TOTP-d visszaállÃtásra került.", "Your_workspace_is_ready": "A munkaterület készen áll a 🎉 használatára" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json index c45684989d6..a7df1bc4e38 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json @@ -1642,6 +1642,7 @@ "manage-assets_description": "Izin mengelola aset server", "manage-emoji": "Mengelola Emoji", "manage-emoji_description": "Izin mengelola server emojis", + "messages_pruned": "pesan dipangkas", "manage-integrations": "Kelola Integrasi", "manage-integrations_description": "Izin untuk mengelola integrasi server", "manage-oauth-apps": "Kelola Aplikasi Oauth", @@ -1753,7 +1754,6 @@ "Message_view_mode_info": "Hal ini akan mengubah jumlah pesan ruang mengambil di layar.", "messages": "Pesan", "Messages": "Pesan", - "messages_pruned": "pesan dipangkas", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Pesan yang dikirim ke masuk WebHook akan diposting di sini.", "Meta": "Meta", "Meta_custom": "Tag Meta Khusus", @@ -2803,4 +2803,4 @@ "Your_push_was_sent_to_s_devices": "push dikirim ke%s perangkat", "Your_server_link": "Tautan server Anda", "Your_workspace_is_ready": "Ruang kerja Anda siap digunakan 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index cfe5dd28766..4e659c3e99f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -1690,6 +1690,7 @@ "manage-assets_description": "Autorizzazione a gestire le risorse del server", "manage-emoji": "Gestisci Emoji", "manage-emoji_description": "Autorizzazione a gestire gli emoji del server", + "messages_pruned": "messaggi cancellati", "manage-integrations": "Gestisci le integrazioni", "manage-integrations_description": "Autorizzazione a gestire le integrazioni del server", "manage-oauth-apps": "Gestisci app Oauth", @@ -1805,7 +1806,6 @@ "Message_view_mode_info": "Questo cambia il numero di spazio nei messaggi sullo schermo.", "messages": "Messaggi", "Messages": "Messaggi", - "messages_pruned": "messaggi cancellati", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "I messaggi inviati WebHook In Arrivo verranno pubblicati qui.", "Meta": "Meta", "Meta_custom": "Meta tag personalizzati", @@ -2895,4 +2895,4 @@ "Your_push_was_sent_to_s_devices": "La tua richiesta è stata inviata ai %s dispositivi.", "Your_server_link": "Il tuo collegamento al server", "Your_workspace_is_ready": "Il tuo spazio di lavoro è pronto per l'uso 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index d451088e627..95662c1ad88 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -2793,6 +2793,7 @@ "manage-email-inbox_description": "å—信トレイを管ç†ã™ã‚‹æ¨©é™", "manage-emoji": "絵文å—ã®ç®¡ç†", "manage-emoji_description": "サーãƒãƒ¼ã®çµµæ–‡å—を管ç†ã™ã‚‹æ¨©é™", + "messages_pruned": "æ•´ç†ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸", "manage-incoming-integrations": "ç€ä¿¡çµ±åˆã®ç®¡ç†", "manage-incoming-integrations_description": "サーãƒãƒ¼ã®ç€ä¿¡çµ±åˆã‚’管ç†ã™ã‚‹æ¨©é™", "manage-integrations": "çµ±åˆã®ç®¡ç†", @@ -2996,7 +2997,6 @@ "MessageBox_view_mode": "メッセージボックス表示モード", "messages": "メッセージ", "Messages": "メッセージ", - "messages_pruned": "æ•´ç†ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸", "Messages_sent": "é€ä¿¡ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "å—ä¿¡Webhookã¸é€ä¿¡ã•ã‚Œã‚‹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯ã€ã“ã¡ã‚‰ã¸æŠ•ç¨¿ã•ã‚Œã¾ã™ã€‚", "Meta": "メタ", @@ -4924,4 +4924,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "一部ã®ã‚µãƒ¼ãƒ“スã¯åˆ©ç”¨ã§ããªã„ã‹ã€æ‰‹å‹•ã§è¨å®šã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™", "onboarding.form.standaloneServerForm.publishOwnApp": "プッシュ通知をé€ä¿¡ã™ã‚‹ã«ã¯ã€ç‹¬è‡ªã®ã‚¢ãƒ—リをコンパイルã—ã¦Google Playã¨App Storeã«å…¬é–‹ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™", "onboarding.form.standaloneServerForm.manuallyIntegrate": "外部サービスã¨æ‰‹å‹•ã§çµ±åˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index a9d54e4a22f..89eae1880bf 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -3716,4 +3716,4 @@ "Your_server_link": "თქვენი სერვერის მისáƒáƒ›áƒáƒ თი", "Your_temporary_password_is_password": "თქვენი დრáƒáƒ”ბითი პáƒáƒ áƒáƒšáƒ˜áƒ áƒáƒ ის <strong>[password]</strong>", "Your_workspace_is_ready": "თქვენი სáƒáƒ›áƒ£áƒ¨áƒáƒ გáƒáƒ ემრმზáƒáƒ“ áƒáƒ ის სáƒáƒ›áƒ£áƒ¨áƒáƒáƒ“ 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index f1703dcacf6..d229153e6bb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1934,6 +1934,7 @@ "manage-assets_description": "ការអនុញ្ញាážáž‚្រប់គ្រងទ្រព្យសម្បážáŸ’ážáž·áž˜áŸ‰áž¶ážŸáŸŠáž¸áž“មáŸ", "manage-emoji": "គ្រប់គ្រងសញ្ញាអារម្មណáŸ", "manage-emoji_description": "សិទ្ធិក្នុងការគ្រប់គ្រង emojis ម៉ាស៊ីនមáŸ", + "messages_pruned": "សារដែលបានកាážáŸ‹áž…áŸáž‰", "manage-integrations": "គ្រប់គ្រងសមាហរណកម្ម", "manage-integrations_description": "សិទ្ធិគ្រប់គ្រងសមាហរណកម្មម៉ាស៊ីនមáŸ", "manage-oauth-apps": "គ្រប់គ្រងកម្មវិធី Oauth", @@ -2057,7 +2058,6 @@ "Message_view_mode_info": "áž“áŸáŸ‡áž“ឹងផ្លាស់ប្ážáž¼áž…ំនួននៃសារដែលមានទំហំយកឡើងនៅលើអáŸáž€áŸ’រង់។", "messages": "សារ", "Messages": "សារ", - "messages_pruned": "សារដែលបានកាážáŸ‹áž…áŸáž‰", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "សារដែលážáŸ’រូវបានបញ្ជូនទៅចូល WebHook នឹងážáŸ’រូវបានបង្ហោះនៅទីនáŸáŸ‡áŸ”", "Meta": "មáŸážáž¶", "Meta_custom": "ស្លាកមáŸážáž¶áž•áŸ’ទាល់ážáŸ’លួន", @@ -3152,4 +3152,4 @@ "Your_push_was_sent_to_s_devices": "ការជំរុញរបស់អ្នកážáŸ’រូវបានបញ្ជូនទៅកាន់ឧបករណ០%s បាន", "Your_server_link": "ážáŸ†ážŽáž—្ជាប់ម៉ាស៊ីនមáŸážšáž”ស់អ្នក", "Your_workspace_is_ready": "កន្លែងធ្វើការរបស់អ្នករួចរាល់ដើម្បីប្រើ🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index f84e673afd4..7b80b572658 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -2408,6 +2408,7 @@ "manage-cloud_description": "í´ë¼ìš°ë“œ 관리", "manage-emoji": "ì´ëª¨ì§€ 관리", "manage-emoji_description": "서버 ì´ëª¨ì§€ 관리 권한", + "messages_pruned": "메시지 ì •ë¦¬ë¥¼ 완료했습니다.", "manage-incoming-integrations": "Incomming Integration 관리", "manage-incoming-integrations_description": "서버 Incomming Integrationì„ ê´€ë¦¬ í• ìˆ˜ìžˆëŠ” 권한", "manage-integrations": "통합 관리", @@ -2576,7 +2577,6 @@ "MessageBox_view_mode": "메시지박스 보기 모드", "messages": "메시지", "Messages": "메시지", - "messages_pruned": "메시지 ì •ë¦¬ë¥¼ 완료했습니다.", "Messages_sent": "보낸 메시지", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Webhook으로 들어오는 메시지가 ì´ê³³ì— 게시ë©ë‹ˆë‹¤.", "Meta": "ë©”íƒ€ì •ë³´", @@ -4083,4 +4083,4 @@ "Your_server_link": "서버 ë§í¬", "Your_temporary_password_is_password": "ìž„ì‹œ 비밀번호는 <strong> [password] </strong>입니다.", "Your_workspace_is_ready": " Workspace를 ì‚¬ìš©í• ì¤€ë¹„ê°€ ë˜ì—ˆìŠµë‹ˆë‹¤." -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json index 3c2a061371d..2a136c2b27e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -1637,6 +1637,7 @@ "manage-assets_description": "Destûra birêvebirina malperên serverê", "manage-emoji": "Emoji birêve bike", "manage-emoji_description": "Destûra birêvebirina emojis server", + "messages_pruned": "mesaj", "manage-integrations": "Integrasyonê bikin", "manage-integrations_description": "Destûra birêvekirina sazkirina întegrasyonê", "manage-oauth-apps": "Rêveberên Oauth Manage", @@ -1749,7 +1750,6 @@ "Message_view_mode_info": "Ev guhertin li gorî mêjera mesajên space xwe bavêjin û xwe li ser ekranê.", "messages": "Messages", "Messages": "Messages", - "messages_pruned": "mesaj", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "ئەو نامانەی ئەنێردرێن بۆ WebHookÛ• هاتوەکان لێرە دا ئەنرێت", "Meta": "Meta", "Meta_custom": "Meta Taybet", @@ -2787,4 +2787,4 @@ "Your_push_was_sent_to_s_devices": "push xwe ji bo cîhazên %s hate ÅŸandin", "Your_server_link": "Girêdana serverê", "Your_workspace_is_ready": "Karên te yên amadekar e amade ye" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json index 2f4eaab9fe7..af6a087c02d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -1679,6 +1679,7 @@ "manage-assets_description": "àºàº²àº™àºàº°àº™àº¸àºàº²àº”ໃນàºàº²àº™àº„ຸ້ມຄàºàº‡àºŠàº±àºšàºªàº´àº™àº‚àºàº‡à»€àº„ື່àºàº‡à»àº¡à»ˆàº‚່າàº", "manage-emoji": "Manage Emoji", "manage-emoji_description": "àºàº²àº™àºàº°àº™àº¸àºàº²àº”ໃນàºàº²àº™àº„ຸ້ມຄàºàº‡ emojis ເຊີຟເວີ", + "messages_pruned": "ຂà»à»‰àº„ວາມຖືàºàº•àº±àº”àºàºàº", "manage-integrations": "Manage Integrations", "manage-integrations_description": "àºàº²àº™àºàº°àº™àº¸àºàº²àº”ໃນàºàº²àº™àº„ຸ້ມຄàºàº‡àºàº²àº™à»€àºŠàº·à»ˆàºàº¡à»‚àºàº‡àº‚àºàº‡à»€àº„ື່àºàº‡à»àº¡à»ˆàº‚່າàº", "manage-oauth-apps": "ຈັດàºàº²àº™ Apps Oauth", @@ -1790,7 +1791,6 @@ "Message_view_mode_info": "àºàº²àº™àº›à»ˆàº½àº™à»àº›àº‡àº™àºµà»‰àºˆà»àº²àº™àº§àº™àº‚àºàº‡àº‚à»à»‰àº„ວາມຊ່àºàº‡àº—ີ່ໃຊ້ເວລາເຖິງໃນຫນ້າຈà».", "messages": "ຂà»à»‰àº„ວາມ", "Messages": "ຂà»à»‰àº„ວາມ", - "messages_pruned": "ຂà»à»‰àº„ວາມຖືàºàº•àº±àº”àºàºàº", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "ຂà»à»‰àº„ວາມທີ່ຖືàºàºªàº»à»ˆàº‡à»„ປທີ່ເຂົ້າມາ WebHook ຈະໄດ້ຮັບàºàº²àº™àºˆàº±àº”ພີມມາທີ່ນີ້.", "Meta": "Meta", "Meta_custom": "Custom Meta Tags", @@ -2830,4 +2830,4 @@ "Your_push_was_sent_to_s_devices": "àºàº²àº™àºŠàº¸àºàºàº¹à»‰àº‚àºàº‡àº—່ານໄດ້ຖືàºàºªàº»à»ˆàº‡à»„ປàºàº¸àº›àº°àºàºàº™ %s", "Your_server_link": "ເຊື່àºàº¡àº•à»à»ˆà»€àºŠàºµàºŸà»€àº§àºµàº‚àºàº‡àº—່ານ", "Your_workspace_is_ready": "ພື້ນທີ່ເຮັດວຽàºàº‚àºàº‡àº—່ານà»àº¡à»ˆàº™àºžà»‰àºàº¡àº—ີ່ຈະໃຊ້🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json index dae49fc65b0..5098d693478 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json @@ -1698,6 +1698,7 @@ "manage-assets_description": "Leidimas tvarkyti serverio turtÄ…", "manage-emoji": "Valdykite \"Emoji\"", "manage-emoji_description": "Leidimas valdyti serverio emojis", + "messages_pruned": "praneÅ¡imai suskaidomi", "manage-integrations": "Valdyti integracijas", "manage-integrations_description": "Leidimas valdyti serverio integracijÄ…", "manage-oauth-apps": "Valdykite \"Oauth Apps\"", @@ -1809,7 +1810,6 @@ "Message_view_mode_info": "Tai pakeiÄia kosminių praneÅ¡imų kiekį ekrane.", "messages": "ŽinutÄ—s", "Messages": "ŽinutÄ—s", - "messages_pruned": "praneÅ¡imai suskaidomi", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "PraneÅ¡imai, siunÄiami į \"Incoming WebHook\", bus paskelbti Äia.", "Meta": "Meta", "Meta_custom": "Tinkinti meta žymeles", @@ -2850,4 +2850,4 @@ "Your_push_was_sent_to_s_devices": "JÅ«sų paspaudimas buvo iÅ¡siųstas į%s įrenginius", "Your_server_link": "JÅ«sų serverio nuoroda", "Your_workspace_is_ready": "JÅ«sų darbo vieta yra paruoÅ¡ta naudoti 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json index ab3504fd4b5..7793fa303be 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json @@ -1656,6 +1656,7 @@ "manage-assets_description": "Atļauja pÄrvaldÄ«t servera aktÄ«vus", "manage-emoji": "PÄrvaldÄ«t Emoji", "manage-emoji_description": "Atļauja pÄrvaldÄ«t servera emoji", + "messages_pruned": "ziņojumi ir apgriezti", "manage-integrations": "PÄrvaldÄ«t integrÄciju", "manage-integrations_description": "Atļauja pÄrvaldÄ«t servera integrÄcijjas", "manage-oauth-apps": "PÄrvaldt Oauth lietotnes", @@ -1767,7 +1768,6 @@ "Message_view_mode_info": "Tas maina to cik daudz vietas ziņojumi aizņem uz ekrÄna.", "messages": "Ziņojumi", "Messages": "Ziņojumi", - "messages_pruned": "ziņojumi ir apgriezti", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Ziņojumi, kas tiek sÅ«tÄ«ti uz IenÄkoÅ¡o WebHook, tiks ievietoti Å¡eit.", "Meta": "Meta", "Meta_custom": "PielÄgoti meta tagi", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json index dbaea717cb2..45c4accc3bb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -1638,6 +1638,7 @@ "manage-assets_description": "Серверийн активыг удирдах зөвшөөрөл", "manage-emoji": "Emoji-г удирдах", "manage-emoji_description": "Сервер emojis удирдах зөвшөөрөл", + "messages_pruned": "зурваÑууд нь ÑалгагдÑан", "manage-integrations": "Интеграцийг удирдах", "manage-integrations_description": "Серверийн холболтыг удирдах зөвшөөрөл", "manage-oauth-apps": "Oauth апп-уудыг удирдах", @@ -1749,7 +1750,6 @@ "Message_view_mode_info": "ÐÐ½Ñ Ð½ÑŒ дÑлгÑц дÑÑÑ€ хийгдÑÑ… Ñанах ойн Ñ…ÑмжÑÑ Ó©Ó©Ñ€Ñ‡Ð»Ó©Ð³Ð´Ð´Ó©Ð³.", "messages": "МеÑÑежүүд", "Messages": "МеÑÑежүүд", - "messages_pruned": "зурваÑууд нь ÑалгагдÑан", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Ирж буй WebHook руу илгÑÑгдÑÑн зурваÑууд Ñнд тавигдана.", "Meta": "Мета", "Meta_custom": "Custom Meta Tags", @@ -2784,4 +2784,4 @@ "Your_push_was_sent_to_s_devices": "Таны түлхÑлт %s төхөөрөмж Ñ€Ò¯Ò¯ илгÑÑгдÑÑн", "Your_server_link": "Таны Ñерверийн холбооÑ", "Your_workspace_is_ready": "Таны ажлын талбарыг ашиглахад бÑлÑн байна" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index 0a7f224c90b..83ed3aca5ef 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1640,6 +1640,7 @@ "manage-assets_description": "Kebenaran untuk menguruskan aset pelayan", "manage-emoji": "Urus Emoji", "manage-emoji_description": "Kebenaran untuk menguruskan emojis pelayan", + "messages_pruned": "mesej dipangkas", "manage-integrations": "Urus Integrasi", "manage-integrations_description": "Kebenaran untuk menguruskan penyepaduan pelayan", "manage-oauth-apps": "Urus Oauth Apps", @@ -1752,7 +1753,6 @@ "Message_view_mode_info": "Ini mengubah jumlah mesej ruang mengambil pada skrin.", "messages": "Mesej", "Messages": "Mesej", - "messages_pruned": "mesej dipangkas", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Mesej-mesej yang dihantar ke masuk WebHook akan disiarkan di sini.", "Meta": "Meta", "Meta_custom": "Tag Meta Custom", @@ -2799,4 +2799,4 @@ "Your_push_was_sent_to_s_devices": "push anda telah dihantar ke peranti %s", "Your_server_link": "Pautan pelayan anda", "Your_workspace_is_ready": "Ruang kerja anda sedia untuk menggunakan 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 239181c2646..566acf7d749 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -2800,6 +2800,7 @@ "manage-email-inbox_description": "Toestemming om e-mailinboxen te beheren", "manage-emoji": "Beheer Emoji", "manage-emoji_description": "Toestemming om de serveremoji's te beheren", + "messages_pruned": "berichten gesnoeid", "manage-incoming-integrations": "Beheer inkomende integraties", "manage-incoming-integrations_description": "Toestemming om de inkomende integraties van de server te beheren", "manage-integrations": "Beheer Integraties", @@ -3004,7 +3005,6 @@ "MessageBox_view_mode": "MessageBox-weergavemodus", "messages": "berichten", "Messages": "Berichten", - "messages_pruned": "berichten gesnoeid", "Messages_sent": "Berichten verzonden", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Berichten die naar de inkomende WebHook worden gestuurd, worden hier gepost.", "Meta": "Meta", @@ -4948,4 +4948,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Sommige diensten zullen niet beschikbaar zijn of vereisen handmatige configuratie", "onboarding.form.standaloneServerForm.publishOwnApp": "Om pushmeldingen te verzenden, moet u uw eigen app compileren en publiceren in Google Play en App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Moet handmatig worden geïntegreerd met externe services" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json index 6b2b431afc4..f2198f03712 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1717,6 +1717,7 @@ "manage-assets_description": "Tillatelse til Ã¥ administrere serverenes eiendeler", "manage-emoji": "Administrer Emoji", "manage-emoji_description": "Tillatelse til Ã¥ administrere server emojis", + "messages_pruned": "meldinger beskjæres", "manage-integrations": "Administrer integrasjoner", "manage-integrations_description": "Tillatelse til Ã¥ administrere serverintegrasjonene", "manage-oauth-apps": "Administrer Oauth Apps", @@ -1833,7 +1834,6 @@ "Message_view_mode_info": "Dette endrer mengden plassmeldinger som tas opp pÃ¥ skjermen.", "messages": "meldinger", "Messages": "meldinger", - "messages_pruned": "meldinger beskjæres", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", "Meta_custom": "Egendefinerte Meta Tags", @@ -2900,4 +2900,4 @@ "Your_push_was_sent_to_s_devices": "Din push ble sendt til%s-enheter", "Your_server_link": "Din serverkobling", "Your_workspace_is_ready": "Ditt arbeidsomrÃ¥de er klar til bruk 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 25fb656f75f..58852027a54 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -2873,6 +2873,7 @@ "manage-email-inbox_description": "Uprawnienia do zarzÄ…dzania skrzynkami Email", "manage-emoji": "ZarzÄ…dzaj emotikonami", "manage-emoji_description": "Uprawnienia do zarzÄ…dzania emotikonami z serwera", + "messages_pruned": "wiadomoÅ›ci zostaÅ‚y przyciÄ™te", "manage-incoming-integrations": "ZarzÄ…dzaj integracjami przychodzÄ…cymi", "manage-incoming-integrations_description": "Uprawnienie do zarzÄ…dzania integracjami przychodzÄ…cymi", "manage-integrations": "ZarzÄ…dzaj integracjami", @@ -3083,7 +3084,6 @@ "MessageBox_view_mode": "Tryb widoku MessageBox", "messages": "WiadomoÅ›ci", "Messages": "WiadomoÅ›ci", - "messages_pruned": "wiadomoÅ›ci zostaÅ‚y przyciÄ™te", "Messages_selected": "Wybrane wiadomoÅ›ci", "Messages_sent": "WiadomoÅ›ci wysÅ‚ane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "WiadomoÅ›ci, które zostanÄ… przesÅ‚ane przez WebHook bÄ™dÄ… publikowane tutaj.", @@ -5076,4 +5076,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usÅ‚ug bÄ™dÄ… niedostÄ™pne lub bÄ™dÄ… wymagaÅ‚y rÄ™cznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyÅ‚ania powiadomieÅ„ push należy skompilować i opublikować wÅ‚asnÄ… aplikacjÄ™ w Google Play i App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Konieczność rÄ™cznej integracji z usÅ‚ugami zewnÄ™trznymi" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index c4549297d87..f2cc4d44e6a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2804,6 +2804,7 @@ "manage-email-inbox_description": "Permissão para gerenciar caixas de entrada de e-mail", "manage-emoji": "Gerenciar emoji", "manage-emoji_description": "Permissão para gerenciar emojis", + "messages_pruned": "mensagens removidas", "manage-incoming-integrations": "Gerenciar integrações de entrada", "manage-incoming-integrations_description": "Permissão para gerenciar integrações de entrada do servidor", "manage-integrations": "Gerenciar integrações", @@ -3009,7 +3010,6 @@ "MessageBox_view_mode": "Modo de visualização MessageBox", "messages": "mensagens", "Messages": "Mensagens", - "messages_pruned": "mensagens removidas", "Messages_sent": "Mensagens enviadas", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "As mensagens que são enviadas para o WebHook de entrada serão publicadas aqui.", "Meta": "Meta", @@ -4958,4 +4958,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponÃveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "É necessário integrar manualmente com serviços externos" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 91381b26333..7875107b8ab 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1929,6 +1929,7 @@ "manage-assets_description": "Permissão para gerir os activos do servidor", "manage-emoji": "Gerir Emoji", "manage-emoji_description": "Permissão para gerir o servidor emojis", + "messages_pruned": "mensagens removidas", "manage-integrations": "Gerir Integrações", "manage-integrations_description": "Permissão para gerir as integrações do servidor", "manage-oauth-apps": "Gerir aplicações Oauth", @@ -2050,7 +2051,6 @@ "Message_view_mode_info": "Isto altera o espaço que as mensagens ocupam no ecrã.", "messages": "Mensagens", "Messages": "Mensagens", - "messages_pruned": "mensagens removidas", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "As mensagens que são enviadas para o WebHook de Entrada serão publicadas aqui.", "Meta": "Meta", "Meta_custom": "Custom Tags Meta", @@ -3214,4 +3214,4 @@ "Your_question": "A sua pergunta", "Your_server_link": "O link do seu servidor", "Your_workspace_is_ready": "O seu espaço de trabalho está pronto a usar 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json index 6d03bf203ba..5744d1fd5bd 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -1641,6 +1641,7 @@ "manage-assets_description": "Permisiunea de a gestiona activele serverului", "manage-emoji": "Administrează Emoji", "manage-emoji_description": "Permisiunea de a gestiona serverul emojis", + "messages_pruned": "mesaje trimise", "manage-integrations": "GestionaÈ›i integrarea", "manage-integrations_description": "Permisiunea de a gestiona integrarea serverului", "manage-oauth-apps": "GestionaÈ›i aplicaÈ›iile Oauth", @@ -1753,7 +1754,6 @@ "Message_view_mode_info": "Acest lucru se schimba cantitatea mesajelor de spaÈ›iu ocupă pe ecran.", "messages": "Mesaje", "Messages": "Mesaje", - "messages_pruned": "mesaje trimise", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Mesajele care sunt trimise prin WebHook vor fi postate aici.", "Meta": "Meta", "Meta_custom": "Custom Meta Tags", @@ -2791,4 +2791,4 @@ "Your_push_was_sent_to_s_devices": "Mesajul Push a fost trimis la %s dispozitive", "Your_server_link": "Linkul dvs. de server", "Your_workspace_is_ready": "SpaÈ›iul dvs. de lucru este gata de utilizare 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 9fc65a5de80..10c6394f88e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -2817,6 +2817,7 @@ "manage-email-inbox_description": "Разрешение на управление входÑщей Ñлектронной почтой", "manage-emoji": "Управление Ñмайлами", "manage-emoji_description": "Разрешение на управление Ñмайлами Ñервера", + "messages_pruned": "ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ñ‹", "manage-incoming-integrations": "УправлÑÑ‚ÑŒ ВходÑщими ИнтеграциÑми", "manage-incoming-integrations_description": "Разрешение на управление входÑщими интеграциÑми Ñервера", "manage-integrations": "УправлÑÑ‚ÑŒ интеграциÑми", @@ -3021,7 +3022,6 @@ "MessageBox_view_mode": "MessageBox опции отображениÑ", "messages": "СообщениÑ", "Messages": "СообщениÑ", - "messages_pruned": "ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ñ‹", "Messages_sent": "Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ñ‹", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "СообщениÑ, отправленные на ВходÑщий WebHook, будут размещены здеÑÑŒ.", "Meta": "Мета", @@ -4998,4 +4998,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Ðекоторые ÑервиÑÑ‹ будут недоÑтупны или потребуетÑÑ Ñ€ÑƒÑ‡Ð½Ð°Ñ Ð½Ð°Ñтройка", "onboarding.form.standaloneServerForm.publishOwnApp": "Чтобы отправлÑÑ‚ÑŒ push-уведомлениÑ, необходимо Ñоздать и опубликовать ÑобÑтвенное приложение в Google Play и App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Ðеобходимо вручную выполнить интеграцию Ñ Ð²Ð½ÐµÑˆÐ½Ð¸Ð¼Ð¸ ÑервиÑами" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json index 0f3372dbc44..1ff93c913b3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json @@ -1652,6 +1652,7 @@ "manage-assets_description": "Povolenie na správu aktÃv servera", "manage-emoji": "Spravujte Emoji", "manage-emoji_description": "Povolenie na správu servera emojis", + "messages_pruned": "správy boli prerezané", "manage-integrations": "Správa integráciÃ", "manage-integrations_description": "Povolenie na správu integrácià serverov", "manage-oauth-apps": "Spravujte aplikácie Oauth", @@ -1763,7 +1764,6 @@ "Message_view_mode_info": "Tým sa zmenà množstvo správ na miesto na obrazovke.", "messages": "Správy", "Messages": "správy", - "messages_pruned": "správy boli prerezané", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Správy, ktoré sa posielajú na adresu WebHook prichádzajú, sa sem zobrazia.", "Meta": "meta", "Meta_custom": "Vlastné znaÄky meta", @@ -2803,4 +2803,4 @@ "Your_push_was_sent_to_s_devices": "Posun bol odoslaný na zariadenia%s", "Your_server_link": "Váš odkaz na server", "Your_workspace_is_ready": "Váš pracovný priestor je pripravený na použitie 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json index b7275a42c2d..5b67c5848ec 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json @@ -1632,6 +1632,7 @@ "manage-assets_description": "Dovoljenje za upravljanje strežniÅ¡kih sredstev", "manage-emoji": "Upravljaj s Äustvenimi simboli", "manage-emoji_description": "Dovoljenje za upravljanje strežniÅ¡kih Äustvenih simbolov", + "messages_pruned": "sporoÄila so obrezana", "manage-integrations": "Upravljaj z integracijami", "manage-integrations_description": "Dovoljenje za upravljanje strežniÅ¡kih integracij", "manage-oauth-apps": "Upravljaj z aplikacijami Oauth", @@ -1743,7 +1744,6 @@ "Message_view_mode_info": "S tem se spremeni koliÄina sporoÄil na zaslonu.", "messages": "SporoÄila", "Messages": "SporoÄila", - "messages_pruned": "sporoÄila so obrezana", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "SporoÄila, ki bodo poslana na dohodni WebHook, bodo objavljena tukaj.", "Meta": "Meta", "Meta_custom": "Meta oznake po meri", @@ -2783,4 +2783,4 @@ "Your_push_was_sent_to_s_devices": "VaÅ¡ potisk je bil poslan v naprave %s", "Your_server_link": "Povezava strežnika", "Your_workspace_is_ready": "VaÅ¡ delovni prostor je pripravljen za uporabo 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json index 7c72591e9d7..3d35601cf37 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -1641,6 +1641,7 @@ "manage-assets_description": "Leja për të menaxhuar asetet e serverit", "manage-emoji": "Menaxho Emoji", "manage-emoji_description": "Leja për të menaxhuar emojis server", + "messages_pruned": "mesazhe të prunuara", "manage-integrations": "Menaxho Integrimet", "manage-integrations_description": "Leja për të menaxhuar integrimet e serverit", "manage-oauth-apps": "Menaxho Apps Oauth", @@ -1753,7 +1754,6 @@ "Message_view_mode_info": "Kjo ndryshon sasia e mesazheve hapësirë ​​të marrë deri në ekran.", "messages": "Mesazhet", "Messages": "Mesazhet", - "messages_pruned": "mesazhe të prunuara", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Mesazhet që janë dërguar tek hyrëse WebHook do të postohen këtu.", "Meta": "Meta", "Meta_custom": "Custom Meta Tags", @@ -2793,4 +2793,4 @@ "Your_push_was_sent_to_s_devices": "shtytje juaj u dërgua në pajisjet %s", "Your_server_link": "Lidhjet e serverit tuaj", "Your_workspace_is_ready": "Hapësira e punës tënde është e gatshme të përdorë 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json index fc3ce74a890..4a73f3a7cad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -1471,6 +1471,7 @@ "manage-assets_description": "Дозвола за управљање имовином Ñервера", "manage-emoji": "Управљај емотиконима", "manage-emoji_description": "Дозвола за управљање емојима Ñервера", + "messages_pruned": "поруке Ñу обрезане", "manage-integrations": "Управљајте интеграцијама", "manage-integrations_description": "Дозвола за управљање ÑерверÑким интеграцијама", "manage-oauth-apps": "Управљајте апликацијама Оаутх", @@ -1581,7 +1582,6 @@ "Message_view_mode_info": "Ово мења Ð¸Ð·Ð½Ð¾Ñ Ð¿Ñ€Ð¾Ñтора порука заузимају на екрану.", "messages": "Поруке", "Messages": "Поруке", - "messages_pruned": "поруке Ñу обрезане", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Поруке које Ñе шаљу Инцоминг ВебХоок ће бити овде поÑтављен.", "Meta": "мета", "Meta_custom": "ЦуÑтом Мета Тагови", @@ -2590,4 +2590,4 @@ "Your_push_was_sent_to_s_devices": "Ваш притиÑком је поÑлат на %s уређајима", "Your_server_link": "Веза Ñа Ñервером", "Your_workspace_is_ready": "Ваш радни проÑтор је Ñпреман за кориштење 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 04f1dd36bdc..2c54a7dc038 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -1714,6 +1714,7 @@ "manage-assets_description": "TillstÃ¥nd att hantera serverns tillgÃ¥ngar", "manage-emoji": "Hantera Emojis", "manage-emoji_description": "TillstÃ¥nd att hantera servern emojis", + "messages_pruned": "meddelanden beskuren", "manage-integrations": "Hantera integreringar", "manage-integrations_description": "TillstÃ¥nd att hantera serverns integreringar", "manage-oauth-apps": "Hantera Oauth Apps", @@ -1826,7 +1827,6 @@ "Message_view_mode_info": "Detta ändrar mängden utrymme meddelanden tar upp pÃ¥ skärmen.", "messages": "Meddelanden", "Messages": "Meddelanden", - "messages_pruned": "meddelanden beskuren", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meddelanden som skickas till inkommande WebHook kommer att publiceras här.", "Meta": "Meta", "Meta_custom": "Anpassade metataggar", @@ -2894,4 +2894,4 @@ "Your_push_was_sent_to_s_devices": "Din push skickades till %s enheter", "Your_server_link": "Din serverlänk", "Your_workspace_is_ready": "Din arbetsyta är redo att använda 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index c0634614920..3edd025013a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -1642,6 +1642,7 @@ "manage-assets_description": "சரà¯à®µà®°à¯ சொதà¯à®¤à¯à®•à®³à¯ˆ நிரà¯à®µà®•à®¿à®•à¯à®• அனà¯à®®à®¤à®¿", "manage-emoji": "ஈமோஜி நிரà¯à®µà®•à®¿", "manage-emoji_description": "சேவையக எமோஜிகளை நிரà¯à®µà®•à®¿à®•à¯à®• அனà¯à®®à®¤à®¿", + "messages_pruned": "செயà¯à®¤à®¿à®•à®³à¯ˆ சீரமைகà¯à®•", "manage-integrations": "à®’à®°à¯à®™à¯à®•à®¿à®£à¯ˆà®ªà¯à®ªà¯à®•à®³à¯ˆ நிரà¯à®µà®•à®¿", "manage-integrations_description": "சேவையக à®’à®°à¯à®™à¯à®•à®¿à®£à¯ˆà®ªà¯à®ªà¯à®•à®³à¯ˆ நிரà¯à®µà®•à®¿à®•à¯à®• அனà¯à®®à®¤à®¿", "manage-oauth-apps": "Oauth பயனà¯à®ªà®¾à®Ÿà¯à®•à®³à¯ˆ நிரà¯à®µà®•à®¿", @@ -1754,7 +1755,6 @@ "Message_view_mode_info": "இநà¯à®¤ இடதà¯à®¤à¯ˆ செயà¯à®¤à®¿à®•à®³à¯ˆ அளவ௠திரையில௠எடà¯à®¤à¯à®¤à¯ மாறà¯à®±à¯à®•à®¿à®±à®¤à¯.", "messages": "செயà¯à®¤à®¿à®•à®³à¯", "Messages": "செயà¯à®¤à®¿à®•à®³à¯", - "messages_pruned": "செயà¯à®¤à®¿à®•à®³à¯ˆ சீரமைகà¯à®•", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "உளà¯à®µà®°à¯à®®à¯ WebHook அனà¯à®ªà¯à®ªà®ªà¯à®ªà®Ÿà¯à®®à¯ எனà¯à®±à¯ செயà¯à®¤à®¿à®•à®³à¯ இஙà¯à®•à¯‡ வெளியிடபà¯à®ªà®Ÿà¯à®®à¯.", "Meta": "மெடà¯à®Ÿà®¾", "Meta_custom": "விரà¯à®ªà¯à®ª மெடà¯à®Ÿà®¾ கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•à®³à¯", @@ -2793,4 +2793,4 @@ "Your_push_was_sent_to_s_devices": "உஙà¯à®•à®³à¯ மிகà¯à®¤à®¿% கள௠சாதனஙà¯à®•à®³à¯ அனà¯à®ªà¯à®ªà®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯", "Your_server_link": "உஙà¯à®•à®³à¯ சரà¯à®µà®°à¯ இணைபà¯à®ªà¯", "Your_workspace_is_ready": "உஙà¯à®•à®³à¯ பணியிடம௠use பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤ தயாராக உளà¯à®³à®¤à¯" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json index 88f57291d34..75e8a081275 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json @@ -1636,6 +1636,7 @@ "manage-assets_description": "สิทธิ์ในà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸ªà¸´à¸™à¸—รัพย์เซิร์ฟเวà¸à¸£à¹Œ", "manage-emoji": "จัดà¸à¸²à¸£ Emoji", "manage-emoji_description": "สิทธิ์ในà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ emojis", + "messages_pruned": "ข้à¸à¸„วาม pruned", "manage-integrations": "จัดà¸à¸²à¸£ Integrations", "manage-integrations_description": "สิทธิ์ในà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸à¸²à¸£à¸œà¸ªà¸²à¸™à¸£à¸§à¸¡à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ", "manage-oauth-apps": "จัดà¸à¸²à¸£à¹à¸à¸› Oauth", @@ -1747,7 +1748,6 @@ "Message_view_mode_info": "à¸à¸²à¸£à¸—ำเช่นนี้จะเปลี่ยนจำนวนเนื้à¸à¸—ี่ว่างบนหน้าจà¸", "messages": "ข้à¸à¸„วาม", "Messages": "ข้à¸à¸„วาม", - "messages_pruned": "ข้à¸à¸„วาม pruned", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "ข้à¸à¸„วามที่ถูà¸à¸ªà¹ˆà¸‡à¹„ปยัง WebHook ขาเข้าจะถูà¸à¸¥à¸‡à¸£à¸²à¸¢à¸à¸²à¸£à¸šà¸±à¸à¸Šà¸µà¸—ี่นี่", "Meta": "Meta", "Meta_custom": "à¹à¸—็ภMeta à¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เà¸à¸‡", @@ -2781,4 +2781,4 @@ "Your_push_was_sent_to_s_devices": "à¸à¸²à¸£à¸à¸”ขà¸à¸‡à¸„ุณถูà¸à¸ªà¹ˆà¸‡à¹„ปยังà¸à¸¸à¸›à¸à¸£à¸“์%s", "Your_server_link": "ลิงค์เซิร์ฟเวà¸à¸£à¹Œà¸‚à¸à¸‡à¸„ุณ", "Your_workspace_is_ready": "พื้นที่ทำงานขà¸à¸‡à¸„ุณพร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¹à¸¥à¹‰à¸§ðŸŽ‰" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index 85a995b0899..eb197123bf4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -1962,6 +1962,7 @@ "manage-cloud_description": "Bulutu Yönet", "manage-emoji": "Emoji Yönetimi", "manage-emoji_description": "Sunucu emojisini yönetme izni", + "messages_pruned": "iletiler budandı", "manage-integrations": "Entegrasyonları Yönetin", "manage-integrations_description": "Sunucu bütünleÅŸtirmelerini yönetme izni", "manage-oauth-apps": "Oauth Uygulamalarını Yönetin", @@ -2089,7 +2090,6 @@ "Message_view_mode_info": "Bu, iletilerin ekranda kaplayacağı alanın boyutunu ayarlar.", "messages": "iletiler", "Messages": "Ä°letiler", - "messages_pruned": "iletiler budandı", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Gelen WebHook gönderilen mesajları burada ilan edilecektir.", "Meta": "Meta", "Meta_custom": "Özel Meta Etiketleri", @@ -3305,4 +3305,4 @@ "Your_question": "Sorunuz", "Your_server_link": "Sunucu baÄŸlantınız", "Your_workspace_is_ready": "Çalışma alanınız kullanılmaya hazır 🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json index 525ed3c9fef..aaa96194183 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json @@ -1239,4 +1239,4 @@ "Your_mail_was_sent_to_s": "يوللاندى %s Ø³Ù‰Ø²Ù†Ù‰Ú Ø¦Ù‰Ù„Ø®Ù‰ØªÙ‰Úىز ئاللىبۇرۇن", "Your_password_is_wrong": "پارول خاتا !", "Your_push_was_sent_to_s_devices": "ئۈسكىنىگە يوللاندى %s Ø³Ù‰Ø²Ù†Ù‰Ú Ø¦Ù‰ØªØªÙ‰Ø±Ú¯Ù‰Ù†Ù‰Úىز" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json index 1c5942872d4..8e71f223768 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -2132,6 +2132,7 @@ "manage-cloud_description": "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ…Ð¼Ð°Ñ€Ð¾ÑŽ", "manage-emoji": "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÐµÐ¼Ð¾Ñ†Ñ–Ñми", "manage-emoji_description": "Дозвіл керувати emojis Ñервера", + "messages_pruned": "Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ñ€Ñ–Ð·Ð°Ð½Ñ–", "manage-integrations": "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ”ÑŽ", "manage-integrations_description": "Дозвіл керувати інтеграцією Ñерверів", "manage-oauth-apps": "Керуйте програмами Oauth", @@ -2266,7 +2267,6 @@ "Message_view_mode_info": "Це змінює кількіÑÑ‚ÑŒ коÑмічних повідомлень займають на екрані.", "messages": "повідомленнÑ", "Messages": "повідомленнÑ", - "messages_pruned": "Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ñ€Ñ–Ð·Ð°Ð½Ñ–", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "ПовідомленнÑ, відправлені до вхідного WebHook будуть опубліковані тут.", "Meta": "Мета", "Meta_custom": "КориÑтувацькі метатеги", @@ -3392,4 +3392,4 @@ "Your_server_link": "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° Ваш Ñервер", "Your_temporary_password_is_password": "Ваш тимчаÑовий пароль <strong>[password]</strong>.", "Your_workspace_is_ready": "Ваш робочий проÑÑ‚Ñ–Ñ€ готовий до викориÑÑ‚Ð°Ð½Ð½Ñ ðŸŽ‰" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json index b3bb800880d..592aa1489d5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json @@ -1735,6 +1735,7 @@ "manage-assets_description": "Cho phép quản lý tà i sản máy chủ", "manage-emoji": "Quản lý Emoji", "manage-emoji_description": "Cho phép quản lý máy chủ emojis", + "messages_pruned": "tin nhắn được cắt tỉa", "manage-integrations": "Quản lý TÃch hợp", "manage-integrations_description": "Cho phép quản lý tÃch hợp máy chủ", "manage-oauth-apps": "Quản lý ứng dụng Oauth", @@ -1846,7 +1847,6 @@ "Message_view_mode_info": "Äiá»u nà y thay đổi số lượng tin nhắn không gian chiếm trên mà n hình.", "messages": "Tin nhắn", "Messages": "Tin nhắn", - "messages_pruned": "tin nhắn được cắt tỉa", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Thông báo được gá»i đến WebHook và o sẽ được đăng ở đây.", "Meta": "Meta", "Meta_custom": "Thẻ Meta tùy chỉnh", @@ -2892,4 +2892,4 @@ "Your_push_was_sent_to_s_devices": "Äã được gá»i tá»›i % thiết bị", "Your_server_link": "ÄÆ°á»ng dẫn máy chủ của bạn", "Your_workspace_is_ready": "Workspace của bạn đã sẵn sà ng" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index a1ae3bef5e4..038d177c35b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -1661,6 +1661,7 @@ "manage-assets_description": "管ç†æœåŠ¡å™¨èµ„产的æƒé™", "manage-emoji": "管ç†è¡¨æƒ…符å·", "manage-emoji_description": "管ç†æœåŠ¡å™¨emojisçš„æƒé™", + "messages_pruned": "消æ¯è¢«ä¿®å‰ª", "manage-integrations": "管ç†é›†æˆ", "manage-integrations_description": "管ç†æœåŠ¡å™¨é›†æˆçš„æƒé™", "manage-oauth-apps": "管ç†Oauth应用程åº", @@ -1775,7 +1776,6 @@ "Message_view_mode_info": "这改å˜äº†å±å¹•ä¸Šå 用的空间消æ¯é‡ã€‚", "messages": "消æ¯", "Messages": "消æ¯", - "messages_pruned": "消æ¯è¢«ä¿®å‰ª", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "å‘é€åˆ°ä¼ å…¥WebHook的消æ¯å°†åœ¨æ¤å¤„å‘布。", "Meta": "Meta", "Meta_custom": "è‡ªå®šä¹‰å…ƒæ ‡è®°", @@ -2812,4 +2812,4 @@ "Your_push_was_sent_to_s_devices": "您的推é€å·²å‘é€åˆ°ï¼…s设备", "Your_server_link": "您的æœåŠ¡å™¨é“¾æŽ¥", "Your_workspace_is_ready": "您的工作区已准备好使用🎉" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index 8c74beed7af..a58c4aebe8b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -2719,6 +2719,7 @@ "manage-cloud_description": "管ç†é›²ç«¯", "manage-emoji": "管ç†è¡¨æƒ…符號", "manage-emoji_description": "管ç†æœå‹™å™¨emojis的權é™", + "messages_pruned": "訊æ¯è¢«ä¿®å‰ª", "manage-incoming-integrations": "管ç†é€£å…¥æ•´åˆ", "manage-incoming-integrations_description": "有權é™ç®¡ç†ä¼ºæœå™¨é€£å…¥æ•´åˆ", "manage-integrations": "管ç†æ•´åˆ", @@ -2916,7 +2917,6 @@ "MessageBox_view_mode": "MessageBox 檢視模å¼", "messages": "訊æ¯", "Messages": "訊æ¯", - "messages_pruned": "訊æ¯è¢«ä¿®å‰ª", "Messages_sent": "訊æ¯ç™¼é€", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "傳é€è‡³é€£å…¥ WebHook 的訊æ¯æœƒé¡¯ç¤ºåœ¨é€™è£¡ã€‚", "Meta": "Meta", @@ -4646,4 +4646,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "部分æœå‹™å°‡ç„¡æ³•ä½¿ç”¨æˆ–是需è¦æ‰‹å‹•è¨å®š", "onboarding.form.standaloneServerForm.publishOwnApp": "è‹¥è¦å‚³é€æŽ¨æ’é€šçŸ¥ï¼Œæ‚¨å¿…é ˆå°æ‚¨æ‰€æ“有的應用程å¼é€²è¡Œç·¨ç¢¼ï¼Œä¸¦å°‡æ‡‰ç”¨ç¨‹å¼ç™¼ä½ˆè‡³ Google Play å’Œ App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "需è¦æ‰‹å‹•èˆ‡å¤–部æœå‹™æ•´åˆ" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 1fdbc885178..fbfd77b2983 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -2452,6 +2452,7 @@ "manage-email-inbox_description": "管ç†ç”µå邮件收件箱的æƒé™", "manage-emoji": "管ç†è¡¨æƒ…符å·", "manage-emoji_description": "管ç†æœåŠ¡å™¨è¡¨æƒ…çš„æƒé™", + "messages_pruned": "消æ¯è¢«ä¿®å‰ª", "manage-incoming-integrations": "管ç†è¿žå…¥é›†æˆ", "manage-incoming-integrations_description": "管ç†æœåŠ¡å™¨å…¥ç«™é›†æˆçš„æƒé™", "manage-integrations": "管ç†é›†æˆ", @@ -2625,7 +2626,6 @@ "MessageBox_view_mode": "消æ¯æ¡†æŸ¥çœ‹æ¨¡å¼", "messages": "消æ¯", "Messages": "消æ¯", - "messages_pruned": "消æ¯è¢«ä¿®å‰ª", "Messages_sent": "å·²å‘é€æ¶ˆæ¯", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "入站 WebHook 的消æ¯ä¼šå‘布到这里。", "Meta": "Meta", @@ -4189,4 +4189,4 @@ "Your_temporary_password_is_password": "您的暂时密ç 为 <strong>[password]</strong>。", "Your_TOTP_has_been_reset": "您的两æ¥éªŒè¯ TOTP å·²é‡ç½®ã€‚", "Your_workspace_is_ready": "您的工作区已准备好使用🎉" -} +} \ No newline at end of file -- GitLab From bf915717dbb1d0f4ba46df71f682fbb815ddcbe2 Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Sat, 3 Sep 2022 07:16:14 -0300 Subject: [PATCH 004/107] [FIX] Typo on new homepage (#26768) --- apps/meteor/client/views/home/cards/DesktopAppsCard.tsx | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx index d625bedffac..b2842a33ba8 100644 --- a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx +++ b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx @@ -13,7 +13,7 @@ const DesktopAppsCard = (): ReactElement => { return ( <Card variant='light' data-qa-id='homepage-desktop-apps-card'> <Card.Title>{t('Desktop_apps')}</Card.Title> - <Card.Body>{t('Install_rocket_chat_on_the_your_preferred_desktop_platform')}</Card.Body> + <Card.Body>{t('Install_rocket_chat_on_your_preferred_desktop_platform')}</Card.Body> <Card.FooterWrapper> <Card.Footer> <ExternalLink to={WINDOWS_APP_URL}> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 8dfae678d7f..768c66b0773 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2446,7 +2446,7 @@ "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message", "Insert_Contact_Name": "Insert the Contact Name", "Insert_Placeholder": "Insert Placeholder", - "Install_rocket_chat_on_the_your_preferred_desktop_platform": "Install Rocket.Chat on the your preferred desktop platform.", + "Install_rocket_chat_on_your_preferred_desktop_platform": "Install Rocket.Chat on your preferred desktop platform.", "Insurance": "Insurance", "Integration_added": "Integration has been added", "Integration_Advanced_Settings": "Advanced Settings", -- GitLab From 8ebefd8a6926a180697d560d79d2bbcb7a41a2cc Mon Sep 17 00:00:00 2001 From: souzaramon <ramon.souza@objective.com.br> Date: Mon, 5 Sep 2022 12:26:22 -0300 Subject: [PATCH 005/107] Chore: Test/improve reliability (#26724) --- .../EngagementDashboardPage.tsx | 2 +- apps/meteor/tests/e2e/administration.spec.ts | 14 ++--- apps/meteor/tests/e2e/apps.spec.ts | 22 ++++--- .../tests/e2e/engagement-dashboard.spec.ts | 9 +-- apps/meteor/tests/e2e/messaging.spec.ts | 11 +++- .../e2e/omnichannel-currentChats.spec.ts | 61 ++++++++++++------- .../tests/e2e/omnichannel-livechat.spec.ts | 10 ++- .../tests/e2e/page-objects/across-page.ts | 13 ---- apps/meteor/tests/e2e/page-objects/admin.ts | 4 -- .../page-objects/fragments/admin-sidenav.ts | 29 --------- .../page-objects/fragments/home-sidenav.ts | 5 -- .../tests/e2e/page-objects/fragments/index.ts | 1 - .../tests/e2e/page-objects/home-channel.ts | 4 ++ apps/meteor/tests/e2e/page-objects/index.ts | 1 - .../e2e/settings-account-profile.spec.ts | 37 +++++------ apps/meteor/tests/e2e/utils/apps/apps-data.ts | 5 -- .../tests/e2e/utils/apps/cleanup-app.ts | 12 ---- apps/meteor/tests/e2e/utils/apps/index.ts | 3 - .../tests/e2e/utils/apps/install-test-app.ts | 11 ---- .../meteor/tests/e2e/utils/apps/remove-app.ts | 11 ---- .../tests/e2e/utils/create-aux-context.ts | 15 ----- apps/meteor/tests/e2e/utils/index.ts | 2 - 22 files changed, 100 insertions(+), 182 deletions(-) delete mode 100644 apps/meteor/tests/e2e/page-objects/across-page.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/admin-sidenav.ts delete mode 100644 apps/meteor/tests/e2e/utils/apps/apps-data.ts delete mode 100644 apps/meteor/tests/e2e/utils/apps/cleanup-app.ts delete mode 100644 apps/meteor/tests/e2e/utils/apps/index.ts delete mode 100644 apps/meteor/tests/e2e/utils/apps/install-test-app.ts delete mode 100644 apps/meteor/tests/e2e/utils/apps/remove-app.ts delete mode 100644 apps/meteor/tests/e2e/utils/create-aux-context.ts diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index fd25ccb1cea..d11fb25daac 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -32,7 +32,7 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb ); return ( - <Page backgroundColor='neutral-100' data-qa='EngagementDashboardPage'> + <Page backgroundColor='neutral-100'> <Page.Header title={t('Engagement_Dashboard')}> <Select options={timezoneOptions} value={timezoneId} onChange={handleTimezoneChange} /> </Page.Header> diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 7f38e5ad328..c9f1ebd072a 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -10,13 +10,11 @@ test.describe.parallel('administration', () => { test.beforeEach(async ({ page }) => { poAdmin = new Admin(page); - - await page.goto('/admin'); }); test.describe('Info', () => { - test.beforeEach(async () => { - await poAdmin.sidenav.linkInfo.click(); + test.beforeEach(async ({ page }) => { + await page.goto('/admin/info'); }); test('expect download info as JSON', async ({ page }) => { @@ -27,8 +25,8 @@ test.describe.parallel('administration', () => { }); test.describe('Users', () => { - test.beforeEach(async () => { - await poAdmin.sidenav.linkUsers.click(); + test.beforeEach(async ({ page }) => { + await page.goto('/admin/users'); }); test('expect find "user1" user', async ({ page }) => { @@ -50,8 +48,8 @@ test.describe.parallel('administration', () => { }); test.describe('Rooms', () => { - test.beforeEach(async () => { - await poAdmin.sidenav.linkRooms.click(); + test.beforeEach(async ({ page }) => { + await page.goto('/admin/rooms'); }); test('expect find "general" channel', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/apps.spec.ts b/apps/meteor/tests/e2e/apps.spec.ts index b64a6d7a94f..9140cc4ef2c 100644 --- a/apps/meteor/tests/e2e/apps.spec.ts +++ b/apps/meteor/tests/e2e/apps.spec.ts @@ -1,22 +1,26 @@ import { expect, test } from './utils/test'; -import { HomeChannel, AcrossPage } from './page-objects'; -import { installTestApp, cleanupTesterApp } from './utils'; +import { HomeChannel } from './page-objects'; + +const APP_TESTER = { + id: '', + url: 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.5.zip?raw=true', +}; test.use({ storageState: 'user1-session.json' }); test.describe.parallel('Apps', () => { let poHomeChannel: HomeChannel; - let acrossPage: AcrossPage; test.beforeAll(async ({ api }) => { await api.post('/settings/Apps_Framework_enabled', { value: true }); await api.post('/settings/Apps_Framework_Development_Mode', { value: true }); - await installTestApp(api); + + const { app } = await (await api.post('/api/apps', { url: APP_TESTER.url }, '')).json(); + APP_TESTER.id = app.id; }); test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - acrossPage = new AcrossPage(page); await page.goto('/home'); await poHomeChannel.sidenav.openChat('general'); @@ -24,17 +28,17 @@ test.describe.parallel('Apps', () => { test('expect allow user open app contextualbar', async () => { await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); - await expect(acrossPage.btnVerticalBarClose).toBeVisible(); + await expect(poHomeChannel.btnVerticalBarClose).toBeVisible(); }); test('expect app contextualbar to be closed', async () => { await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); - await acrossPage.btnVerticalBarClose.click(); - await expect(acrossPage.btnVerticalBarClose).toBeHidden(); + await poHomeChannel.btnVerticalBarClose.click(); + await expect(poHomeChannel.btnVerticalBarClose).toBeHidden(); }); test.afterAll(async ({ api }) => { - await cleanupTesterApp(api); + await api.delete(`/api/apps/${APP_TESTER.id}`); await api.post('/settings/Apps_Framework_enabled', { value: false }); await api.post('/settings/Apps_Framework_Development_Mode', { value: false }); }); diff --git a/apps/meteor/tests/e2e/engagement-dashboard.spec.ts b/apps/meteor/tests/e2e/engagement-dashboard.spec.ts index 5e43bf7efa6..b8302443437 100644 --- a/apps/meteor/tests/e2e/engagement-dashboard.spec.ts +++ b/apps/meteor/tests/e2e/engagement-dashboard.spec.ts @@ -1,5 +1,4 @@ import { test, expect } from './utils/test'; -import { Admin } from './page-objects'; import { IS_EE } from './config/constants'; test.skip(!IS_EE, 'Engagemente Dashboard > Enterprise Only'); @@ -7,16 +6,10 @@ test.skip(!IS_EE, 'Engagemente Dashboard > Enterprise Only'); test.use({ storageState: 'admin-session.json' }); test.describe('engagement-dashboard', () => { - let poAdmin: Admin; - test.describe.parallel('expect to trigger fallback error component', () => { test.beforeEach(async ({ page }) => { - poAdmin = new Admin(page); - await page.goto('/admin'); - + await page.goto('/admin/engagement-dashboard'); await page.route('**/api/v1/engagement-dashboard/**', (route) => route.abort()); - await poAdmin.sidenav.linkEngagementDashboard.click(); - await expect(page.locator('[data-qa="EngagementDashboardPage"]')).toBeVisible(); }); test('expect to show 4 fallback errors components inside widget at Users Tab', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index ea46ba037f2..77fd96164e2 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -1,6 +1,15 @@ +import type { Browser, Page } from '@playwright/test'; + import { expect, test } from './utils/test'; import { HomeChannel } from './page-objects'; -import { createTargetChannel, createAuxContext } from './utils'; +import { createTargetChannel } from './utils'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + return { page, poHomeChannel }; +}; test.use({ storageState: 'user1-session.json' }); diff --git a/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts b/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts index c45313e8da9..6a0e26adea5 100644 --- a/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts @@ -9,72 +9,87 @@ test.describe.serial('Current Chats', () => { test.beforeEach(async ({ page }) => { pageOmnichannelCurrentChats = new OmnichannelCurrentChats(page); - await page.goto('/omnichannel'); - await pageOmnichannelCurrentChats.sidenav.linkCurrentChats.click(); + await page.goto('/omnichannel/current'); }); - test.describe('Render Fields', () => { - test('expect show guest Field', async () => { + test.describe('Render Fields', async () => { + await test.step('expect show guest Field', async () => { await expect(pageOmnichannelCurrentChats.guestField).toBeVisible(); }); - test('expect show servedBy Field', async () => { + + await test.step('expect show servedBy Field', async () => { await expect(pageOmnichannelCurrentChats.servedByField).toBeVisible(); }); - test('expect show status Field', async () => { + + await test.step('expect show status Field', async () => { await expect(pageOmnichannelCurrentChats.statusField).toBeVisible(); }); - test('expect show from Field', async () => { + + await test.step('expect show from Field', async () => { await expect(pageOmnichannelCurrentChats.fromField).toBeVisible(); }); - test('expect show to Field', async () => { + + await test.step('expect show to Field', async () => { await expect(pageOmnichannelCurrentChats.toField).toBeVisible(); }); - test('expect show department Field', async () => { + + await test.step('expect show department Field', async () => { await expect(pageOmnichannelCurrentChats.departmentField).toBeVisible(); }); - test('expect show form options button', async () => { + + await test.step('expect show form options button', async () => { await expect(pageOmnichannelCurrentChats.formOptions).toBeVisible(); }); }); - test.describe('Render Options', () => { + test.describe('Render Options', async () => { test.beforeEach(async () => { await pageOmnichannelCurrentChats.doOpenOptions(); }); - test('expect show form options button', async () => { + + await test.step('expect show form options button', async () => { await expect(pageOmnichannelCurrentChats.formOptions).toBeVisible(); }); - test('expect show clear filters option', async () => { + + await test.step('expect show clear filters option', async () => { await expect(pageOmnichannelCurrentChats.clearFiltersOption).toBeVisible(); }); - test('expect show removeAllClosed option', async () => { + + await test.step('expect show removeAllClosed option', async () => { await expect(pageOmnichannelCurrentChats.removeAllClosedOption).toBeVisible(); }); - test('expect not to show custom fields option', async () => { + + await test.step('expect not to show custom fields option', async () => { await expect(pageOmnichannelCurrentChats.customFieldsOption).not.toBeVisible(); }); }); - test.describe('Render Table', () => { - test('expect show table', async () => { + test.describe('Render Table', async () => { + await test.step('expect show table', async () => { await expect(pageOmnichannelCurrentChats.table).toBeVisible(); }); - test('expect show name header', async () => { + + await test.step('expect show name header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderName).toBeVisible(); }); - test('expect show department header', async () => { + + await test.step('expect show department header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderDepartment).toBeVisible(); }); - test('expect show servedBy header', async () => { + + await test.step('expect show servedBy header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderServedBy).toBeVisible(); }); - test('expect show startedAt header', async () => { + + await test.step('expect show startedAt header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderStartedAt).toBeVisible(); }); - test('expect show lastMessage header', async () => { + + await test.step('expect show lastMessage header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderLastMessage).toBeVisible(); }); - test('expect show form status header', async () => { + + await test.step('expect show form status header', async () => { await expect(pageOmnichannelCurrentChats.tableHeaderStatus).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts index 9faa92f21b8..cf1fd3fe2d5 100644 --- a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts @@ -1,9 +1,15 @@ import { faker } from '@faker-js/faker'; -import type { Page } from '@playwright/test'; +import type { Browser, Page } from '@playwright/test'; import { test, expect } from './utils/test'; import { HomeChannel, OmnichannelLiveChat } from './page-objects'; -import { createAuxContext } from './utils'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + return { page, poHomeChannel }; +}; const newUser = { name: faker.name.firstName(), diff --git a/apps/meteor/tests/e2e/page-objects/across-page.ts b/apps/meteor/tests/e2e/page-objects/across-page.ts deleted file mode 100644 index 78d97411298..00000000000 --- a/apps/meteor/tests/e2e/page-objects/across-page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class AcrossPage { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get btnVerticalBarClose(): Locator { - return this.page.locator('[data-qa="VerticalBarActionClose"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 2dc803706d5..5e1962bea6b 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -1,18 +1,14 @@ import type { Locator, Page } from '@playwright/test'; -import { AdminSidenav } from './fragments'; import { AdminFlextab } from './fragments/admin-flextab'; export class Admin { private readonly page: Page; - readonly sidenav: AdminSidenav; - readonly tabs: AdminFlextab; constructor(page: Page) { this.page = page; - this.sidenav = new AdminSidenav(page); this.tabs = new AdminFlextab(page); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-sidenav.ts deleted file mode 100644 index 7af87be7603..00000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-sidenav.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class AdminSidenav { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get linkInfo(): Locator { - return this.page.locator('.flex-nav [href="/admin/info"]'); - } - - get linkUsers(): Locator { - return this.page.locator('.flex-nav [href="/admin/users"]'); - } - - get linkRooms(): Locator { - return this.page.locator('.flex-nav [href="/admin/rooms"]'); - } - - get linkSettings(): Locator { - return this.page.locator('.flex-nav [href="/admin/settings"]'); - } - - get linkEngagementDashboard(): Locator { - return this.page.locator('.flex-nav [href="/admin/engagement-dashboard"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index ccc8ed59c15..56361b92ec3 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -31,11 +31,6 @@ export class HomeSidenav { await this.page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]').click(); } - async goToMyAccount(): Promise<void> { - await this.page.locator('[data-qa="sidebar-avatar-button"]').click(); - await this.page.locator('//li[@class="rcx-option"]//div[contains(text(), "My Account")]').click(); - } - async switchStatus(status: 'offline' | 'online'): Promise<void> { await this.page.locator('[data-qa="sidebar-avatar-button"]').click(); await this.page.locator(`//li[@class="rcx-option"]//div[contains(text(), "${status}")]`).click(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index 010641541ee..083829dea7b 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -1,4 +1,3 @@ -export * from './admin-sidenav'; export * from './home-content'; export * from './home-flextab'; export * from './home-sidenav'; diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 786bb41df83..b632df31dde 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -21,4 +21,8 @@ export class HomeChannel { get toastSuccess(): Locator { return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); } + + get btnVerticalBarClose(): Locator { + return this.page.locator('[data-qa="VerticalBarActionClose"]'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index 96881fe2130..0124523a978 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -1,5 +1,4 @@ export * from './account-profile'; -export * from './across-page'; export * from './admin'; export * from './auth'; export * from './home-channel'; diff --git a/apps/meteor/tests/e2e/settings-account-profile.spec.ts b/apps/meteor/tests/e2e/settings-account-profile.spec.ts index d789275b85c..b26a379befd 100644 --- a/apps/meteor/tests/e2e/settings-account-profile.spec.ts +++ b/apps/meteor/tests/e2e/settings-account-profile.spec.ts @@ -19,11 +19,11 @@ test.describe.serial('settings-account-profile', () => { }); // FIXME: solve test intermitencies - test.skip('expect update profile with new name/username', async () => { + test.skip('expect update profile with new name/username', async ({ page }) => { const newName = faker.name.findName(); const newUsername = faker.internet.userName(newName); - await poHomeChannel.sidenav.goToMyAccount(); + await page.goto('/account/profile'); await poAccountProfile.inputName.fill(newName); await poAccountProfile.inputUsername.fill(newUsername); await poAccountProfile.btnSubmit.click(); @@ -39,55 +39,56 @@ test.describe.serial('settings-account-profile', () => { await expect(poHomeChannel.tabs.userInfoUsername).toHaveText(newUsername); }); - test.describe('Personal Access Tokens', () => { - test.beforeEach(async () => { - await poHomeChannel.sidenav.goToMyAccount(); - await poAccountProfile.sidenav.linkTokens.click(); - }); + test('Personal Access Tokens', async ({ page }) => { + await page.goto('/account/tokens'); - test('expect show empty personal access tokens table', async () => { + await test.step('expect show empty personal access tokens table', async () => { await expect(poAccountProfile.tokensTableEmpty).toBeVisible(); await expect(poAccountProfile.inputToken).toBeVisible(); }); - test('expect show new personal token', async () => { + await test.step('expect show new personal token', async () => { await poAccountProfile.inputToken.type(token); await poAccountProfile.btnTokensAdd.click(); await expect(poAccountProfile.tokenAddedModal).toBeVisible(); + await page.locator('button:has-text("Ok")').click(); }); - test('expect not allow add new personal token with same name', async ({ page }) => { + await test.step('expect not allow add new personal token with same name', async () => { await poAccountProfile.inputToken.type(token); await poAccountProfile.btnTokensAdd.click(); await expect(page.locator('.rcx-toastbar.rcx-toastbar--error')).toBeVisible(); }); - test('expect regenerate personal token', async () => { + await test.step('expect regenerate personal token', async () => { await poAccountProfile.tokenInTable(token).locator('button >> nth=0').click(); await poAccountProfile.btnRegenerateTokenModal.click(); await expect(poAccountProfile.tokenAddedModal).toBeVisible(); + await page.locator('button:has-text("Ok")').click(); }); - test('expect delete personal token', async ({ page }) => { + await test.step('expect delete personal token', async () => { await poAccountProfile.tokenInTable(token).locator('button >> nth=1').click(); await poAccountProfile.btnRemoveTokenModal.click(); await expect(page.locator('.rcx-toastbar.rcx-toastbar--success')).toBeVisible(); }); }); - test.describe('Change avatar', () => { - test.beforeEach(async () => { - await poHomeChannel.sidenav.goToMyAccount(); - }); + test('change avatar', async ({ page }) => { + await page.goto('/account/profile'); - test('expect change avatar image by upload', async ({ page }) => { + await test.step('expect change avatar image by upload', async () => { await poAccountProfile.inputImageFile.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); await poAccountProfile.btnSubmit.click(); await expect(page.locator('.rcx-toastbar.rcx-toastbar--success').first()).toBeVisible(); }); - test('expect set image from url', async ({ page }) => { + await test.step('expect to close toastbar', async () => { + await page.locator('.rcx-toastbar.rcx-toastbar--success').first().click(); + }); + + await test.step('expect set image from url', async () => { await poAccountProfile.inputAvatarLink.fill('https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50'); await poAccountProfile.btnSetAvatarLink.click(); diff --git a/apps/meteor/tests/e2e/utils/apps/apps-data.ts b/apps/meteor/tests/e2e/utils/apps/apps-data.ts deleted file mode 100644 index 903776918b5..00000000000 --- a/apps/meteor/tests/e2e/utils/apps/apps-data.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.5.zip?raw=true'; -export const APP_ID = 'bc4dd4a1-bf9b-408e-83a4-aba7eba0bf02'; -export const APP_NAME = 'Apps.RocketChat.Tester'; -export const APP_USERNAME = 'appsrocketchattester.bot'; -export const appsEndpoint = (path = '') => `/api/apps${path}`; diff --git a/apps/meteor/tests/e2e/utils/apps/cleanup-app.ts b/apps/meteor/tests/e2e/utils/apps/cleanup-app.ts deleted file mode 100644 index 4bcebd9e2ba..00000000000 --- a/apps/meteor/tests/e2e/utils/apps/cleanup-app.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { APP_ID } from './apps-data'; -import type { BaseTest } from '../test'; -import { removeAppById } from './remove-app'; - -/** - * cleanupApp: - * - This helper remove the tester app from the workspace - */ - -export const cleanupTesterApp = async (api: BaseTest['api']) => { - await removeAppById(api, APP_ID); -}; diff --git a/apps/meteor/tests/e2e/utils/apps/index.ts b/apps/meteor/tests/e2e/utils/apps/index.ts deleted file mode 100644 index 53671296219..00000000000 --- a/apps/meteor/tests/e2e/utils/apps/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './install-test-app'; -export * from './remove-app'; -export * from './cleanup-app'; diff --git a/apps/meteor/tests/e2e/utils/apps/install-test-app.ts b/apps/meteor/tests/e2e/utils/apps/install-test-app.ts deleted file mode 100644 index c715bb95226..00000000000 --- a/apps/meteor/tests/e2e/utils/apps/install-test-app.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { BaseTest } from '../test'; -import { appsEndpoint, APP_URL } from './apps-data'; - -/** - * installTestApp: - * - Usefull to test apps functionalities - */ - -export async function installTestApp(api: BaseTest['api']): Promise<void> { - await api.post(appsEndpoint(), { url: APP_URL }, ''); -} diff --git a/apps/meteor/tests/e2e/utils/apps/remove-app.ts b/apps/meteor/tests/e2e/utils/apps/remove-app.ts deleted file mode 100644 index 71e29ee17ae..00000000000 --- a/apps/meteor/tests/e2e/utils/apps/remove-app.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { BaseTest } from '../test'; -import { appsEndpoint } from './apps-data'; - -/** - * removeAppById: - * - This helper remove the desirable app from the workspace - */ - -export const removeAppById = async (api: BaseTest['api'], id: string) => { - await api.delete(appsEndpoint(`/${id}`)); -}; diff --git a/apps/meteor/tests/e2e/utils/create-aux-context.ts b/apps/meteor/tests/e2e/utils/create-aux-context.ts deleted file mode 100644 index c97bdf52b44..00000000000 --- a/apps/meteor/tests/e2e/utils/create-aux-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Browser, Page } from '@playwright/test'; - -import { HomeChannel } from '../page-objects'; - -/** - * createAuxContext: - * - Usefull to create a aux context for test will need many contexts - */ - -export const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { - const page = await browser.newPage({ storageState }); - const poHomeChannel = new HomeChannel(page); - await page.goto('/'); - return { page, poHomeChannel }; -}; diff --git a/apps/meteor/tests/e2e/utils/index.ts b/apps/meteor/tests/e2e/utils/index.ts index 0481fe3da21..1939b8b2cb1 100644 --- a/apps/meteor/tests/e2e/utils/index.ts +++ b/apps/meteor/tests/e2e/utils/index.ts @@ -1,3 +1 @@ export * from './create-target-channel'; -export * from './create-aux-context'; -export * from './apps'; -- GitLab From 9a5604b145b053aae9bc67260106f682c7fcc0bb Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:58:11 +0530 Subject: [PATCH 006/107] Chore: Add tests to cover issue fixed in #26720 (#26767) --- .../tests/end-to-end/api/livechat/00-rooms.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 12b2f0b9935..cc5a10d0da2 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -244,6 +244,32 @@ describe('LIVECHAT - rooms', function () { }) .end(done); }); + it('should not cause issues when the customFields is empty', (done) => { + request + .get(api(`livechat/rooms?customFields={}&roomName=test`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.rooms).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + it('should throw an error if customFields param is not a object', (done) => { + request + .get(api(`livechat/rooms?customFields=string`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); }); describe('livechat/room.join', () => { -- GitLab From 78dbb69325bca317c6ee05d9502e7cbf268abc5c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Mon, 5 Sep 2022 17:19:37 -0300 Subject: [PATCH 007/107] Chore: Refactor message list context usage (#26748) --- .../views/room/MessageList/MessageList.tsx | 8 ++++++- .../room/MessageList/components/Message.tsx | 20 ++++++++++------- .../MessageList/components/MessageContent.tsx | 19 ++++++++++------ .../contexts/MessageListContext.tsx | 5 +++++ .../MessageList/hooks/useParsedMessage.ts | 22 +++++++------------ .../providers/MessageListProvider.tsx | 4 ++++ 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index 5b6bc06636e..1cad8505768 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -48,6 +48,10 @@ export const MessageList: FC<{ rid: IRoom['_id'] }> = ({ rid }) => { const isSystemMessage = MessageTypes.isSystemMessage(message); const shouldShowMessage = !isThreadMessage(message) && !isSystemMessage; + const unread = Boolean(subscription?.tunread?.includes(message._id)); + const mention = Boolean(subscription?.tunreadUser?.includes(message._id)); + const all = Boolean(subscription?.tunreadGroup?.includes(message._id)); + return ( <Fragment key={message._id}> {shouldShowDivider && ( @@ -68,7 +72,9 @@ export const MessageList: FC<{ rid: IRoom['_id'] }> = ({ rid }) => { data-qa-type='message' sequential={shouldShowAsSequential} message={message} - subscription={subscription} + unread={unread} + mention={mention} + all={all} /> )} diff --git a/apps/meteor/client/views/room/MessageList/components/Message.tsx b/apps/meteor/client/views/room/MessageList/components/Message.tsx index 7dc54b8fec1..c3e76d0363f 100644 --- a/apps/meteor/client/views/room/MessageList/components/Message.tsx +++ b/apps/meteor/client/views/room/MessageList/components/Message.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { ISubscription, IMessage } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { Message as MessageTemplate, MessageLeftContainer, MessageContainer, MessageBody, CheckBox } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import React, { FC, memo } from 'react'; @@ -14,12 +14,14 @@ import MessageHeader from './MessageHeader'; import { MessageIndicators } from './MessageIndicators'; import Toolbox from './Toolbox'; -const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubscription; id: IMessage['_id'] }> = ({ - message, - sequential, - subscription, - ...props -}) => { +const Message: FC<{ + message: IMessage; + sequential: boolean; + id: IMessage['_id']; + unread: boolean; + mention: boolean; + all: boolean; +}> = ({ message, sequential, all, mention, unread, ...props }) => { const isMessageHighlight = useIsMessageHighlight(message._id); const [isMessageIgnored, toggleMessageIgnored] = useToggle((message as { ignored?: boolean }).ignored ?? false); const { @@ -59,7 +61,9 @@ const Message: FC<{ message: IMessage; sequential: boolean; subscription?: ISubs <MessageContainer> {!sequential && <MessageHeader message={message} />} - {!isMessageIgnored && <MessageContent id={message._id} message={message} subscription={subscription} sequential={sequential} />} + {!isMessageIgnored && ( + <MessageContent id={message._id} message={message} unread={unread} mention={mention} all={all} sequential={sequential} /> + )} {isMessageIgnored && ( <MessageBody data-qa-type='message-body'> <MessageContentIgnored onShowMessageIgnored={toggleMessageIgnored} /> diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx index cfb03baceb4..1e4b10ff67b 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx @@ -21,10 +21,15 @@ import ReactionsList from './MessageReactionsList'; import ReadReceipt from './MessageReadReceipt'; import PreviewList from './UrlPreview'; -const MessageContent: FC<{ message: IMessage; sequential: boolean; subscription?: ISubscription; id: IMessage['_id'] }> = ({ - message, - subscription, -}) => { +const MessageContent: FC<{ + message: IMessage; + sequential: boolean; + subscription?: ISubscription; + id: IMessage['_id']; + unread: boolean; + mention: boolean; + all: boolean; +}> = ({ message, unread, all, mention, subscription }) => { const { broadcast, actions: { openRoom, openThread, replyBroadcast }, @@ -86,9 +91,9 @@ const MessageContent: FC<{ message: IMessage; sequential: boolean; subscription? mid={message._id} rid={message.rid} lm={message.tlm} - unread={Boolean(subscription?.tunread?.includes(message._id))} - mention={Boolean(subscription?.tunreadUser?.includes(message._id))} - all={Boolean(subscription?.tunreadGroup?.includes(message._id))} + unread={unread} + mention={mention} + all={all} participants={message?.replies.length} /> )} diff --git a/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx index d73832d0a04..fef14113ecc 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/MessageListContext.tsx @@ -26,6 +26,8 @@ export type MessageListContextValue = { dollarSyntaxEnabled: boolean; parenthesisSyntaxEnabled: boolean; }; + autoTranslateLanguage?: string; + showColors: boolean; }; export const MessageListContext = createContext<MessageListContextValue>({ @@ -49,6 +51,7 @@ export const MessageListContext = createContext<MessageListContextValue>({ showRoles: false, showRealName: false, showUsername: false, + showColors: false, }); export const useShowTranslated: MessageListContextValue['useShowTranslated'] = (...args) => @@ -79,3 +82,5 @@ export const useOpenEmojiPicker: MessageListContextValue['useOpenEmojiPicker'] = useContext(MessageListContext).useOpenEmojiPicker(...args); export const useReactionsFilter: MessageListContextValue['useReactionsFilter'] = (message: IMessage) => useContext(MessageListContext).useReactionsFilter(message); + +export const useMessageListContext = (): MessageListContextValue => useContext(MessageListContext); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts index 294cdd483af..5bf35a717e3 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts @@ -1,29 +1,23 @@ import { IMessage, isTranslatedMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; import { Root, parse } from '@rocket.chat/message-parser'; -import { useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -import { useShowTranslated } from '../contexts/MessageListContext'; -import { useAutotranslateLanguage } from './useAutotranslateLanguage'; +import { useMessageListContext, useShowTranslated } from '../contexts/MessageListContext'; export function useParsedMessage(message: IMessage & Partial<ITranslatedMessage>): Root { - const colors = useSetting('HexColorPreview_Enabled') as boolean; - const katexEnabled = useSetting('Katex_Enabled') as boolean; - const katexDollarSyntax = useSetting('Katex_Dollar_Syntax') as boolean; - const katexParenthesisSyntax = useSetting('Katex_Parenthesis_Syntax') as boolean; - const autoTranslateLanguage = useAutotranslateLanguage(message.rid); + const { autoTranslateLanguage, katex, showColors } = useMessageListContext(); const translated = useShowTranslated({ message }); - const translations = isTranslatedMessage(message) ? message.translations : undefined; + const translations = isTranslatedMessage(message) && message.translations; const { md, msg } = message; return useMemo(() => { const parseOptions = { - colors, + colors: showColors, emoticons: true, - ...(katexEnabled && { + ...(katex && { katex: { - dollarSyntax: katexDollarSyntax, - parenthesisSyntax: katexParenthesisSyntax, + dollarSyntax: katex.dollarSyntaxEnabled, + parenthesisSyntax: katex.parenthesisSyntaxEnabled, }, }), }; @@ -40,5 +34,5 @@ export function useParsedMessage(message: IMessage & Partial<ITranslatedMessage> } return parse(msg, parseOptions); - }, [colors, katexEnabled, katexDollarSyntax, katexParenthesisSyntax, autoTranslateLanguage, md, msg, translated, translations]); + }, [showColors, katex, autoTranslateLanguage, md, msg, translated, translations]); } diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index cc20fb23310..774c30e8149 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -35,6 +35,7 @@ export const MessageListProvider: FC<{ const katexEnabled = Boolean(useSetting('Katex_Enabled')); const katexDollarSyntaxEnabled = Boolean(useSetting('Katex_Dollar_Syntax')); const katexParenthesisSyntaxEnabled = Boolean(useSetting('Katex_Parenthesis_Syntax')); + const showColors = useSetting('HexColorPreview_Enabled') as boolean; const showRoles = Boolean(!useUserPreference<boolean>('hideRoles') && !isMobile); const showUsername = Boolean(!useUserPreference<boolean>('hideUsernames') && !isMobile); @@ -46,6 +47,7 @@ export const MessageListProvider: FC<{ const context: MessageListContextValue = useMemo( () => ({ + showColors, useReactionsFilter: (message: IMessage): ((reaction: string) => string[]) => { const { reactions } = message; return !showRealName @@ -77,6 +79,7 @@ export const MessageListProvider: FC<{ useShowFollowing: uid ? ({ message }): boolean => Boolean(message.replies && message.replies.indexOf(uid) > -1 && !isThreadMainMessage(message)) : (): boolean => false, + autoTranslateLanguage, useShowTranslated: uid && autoTranslateEnabled && hasSubscription && autoTranslateLanguage ? ({ message }): boolean => @@ -157,6 +160,7 @@ export const MessageListProvider: FC<{ katexParenthesisSyntaxEnabled, highlights, reactToMessage, + showColors, ], ); -- GitLab From 95136291b30d2082e671ca0258327db2e975e88f Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat> Date: Mon, 5 Sep 2022 18:15:02 -0300 Subject: [PATCH 008/107] [FIX] Livechat trigger messages covering all the website (#26776) --- packages/livechat/src/widget.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/livechat/src/widget.js b/packages/livechat/src/widget.js index b809faca0ee..52ed4af994f 100644 --- a/packages/livechat/src/widget.js +++ b/packages/livechat/src/widget.js @@ -70,6 +70,8 @@ function callHook(action, params) { } const updateWidgetStyle = (isOpened) => { + const isFullscreen = smallScreen && widget.dataset.state !== 'triggered'; + if (smallScreen && isOpened) { scrollPosition = document.documentElement.scrollTop; document.body.classList.add('rc-livechat-mobile-full-screen'); @@ -80,8 +82,9 @@ const updateWidgetStyle = (isOpened) => { } } + if (isOpened) { - widget.style.left = smallScreen ? '0' : 'auto'; + widget.style.left = isFullscreen ? '0' : 'auto'; /** * If we use widget.style.height = smallScreen ? '100vh' : ... @@ -91,8 +94,8 @@ const updateWidgetStyle = (isOpened) => { * for widget.style.width */ - widget.style.height = smallScreen ? '100%' : `${ WIDGET_MARGIN + widget_height + WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT }px`; - widget.style.width = smallScreen ? '100%' : `${ WIDGET_MARGIN + WIDGET_OPEN_WIDTH + WIDGET_MARGIN }px`; + widget.style.height = isFullscreen ? '100%' : `${ WIDGET_MARGIN + widget_height + WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT }px`; + widget.style.width = isFullscreen ? '100%' : `${ WIDGET_MARGIN + WIDGET_OPEN_WIDTH + WIDGET_MARGIN }px`; } else { widget.style.left = 'auto'; widget.style.width = `${ WIDGET_MARGIN + WIDGET_MINIMIZED_WIDTH + WIDGET_MARGIN }px`; @@ -151,16 +154,16 @@ const openWidget = () => { } widget_height = WIDGET_OPEN_HEIGHT; - updateWidgetStyle(true); widget.dataset.state = 'opened'; + updateWidgetStyle(true); iframe.focus(); emitCallback('chat-maximized'); }; const resizeWidget = (height) => { widget_height = height; - updateWidgetStyle(true); widget.dataset.state = 'triggered'; + updateWidgetStyle(true); }; function closeWidget() { @@ -168,8 +171,8 @@ function closeWidget() { return; } - updateWidgetStyle(false); widget.dataset.state = 'closed'; + updateWidgetStyle(false); emitCallback('chat-minimized'); } -- GitLab From d44b74e29a0f1b3b88b2d3ca65a736983837af0e Mon Sep 17 00:00:00 2001 From: Martin Schoeler <martin.schoeler@rocket.chat> Date: Mon, 5 Sep 2022 19:13:25 -0300 Subject: [PATCH 009/107] [FIX] Restore current chats default table order (#26808) --- .../client/views/omnichannel/currentChats/CurrentChatsRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 90b206ff463..12332750a4d 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -111,7 +111,7 @@ const useQuery: useQueryType = ( }, [guest, column, direction, itemsPerPage, current, from, to, status, servedBy, department, tags, customFields]); const CurrentChatsRoute = (): ReactElement => { - const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open'>('fname'); + const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open'>('ts', 'desc'); const [customFields, setCustomFields] = useState<{ [key: string]: string }>(); const [params, setParams] = useState({ guest: '', -- GitLab From 94cc0070dba3b6c7e0711fac50c44ee63dabf036 Mon Sep 17 00:00:00 2001 From: Hugo Costa <hugocarreiracosta@gmail.com> Date: Tue, 6 Sep 2022 11:42:31 -0300 Subject: [PATCH 010/107] [FIX] Fix broken legacy message view (#26819) --- apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts b/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts index f84dada5197..fe670ccf3db 100644 --- a/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts +++ b/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts @@ -42,7 +42,7 @@ function messagesHistory() { const query: Mongo.Query<IMessage> = { rid, _hidden: { $ne: true }, - $or: [{ tmid: { $exists: true } }, { tshow: { $eq: true } }], + $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], ...(hideMessagesOfType.size && { t: { $nin: Array.from(hideMessagesOfType.values()) } }), }; -- GitLab From 5240f1624eba4da75506a21a9fdc284e2a843c6e Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Tue, 6 Sep 2022 15:37:40 +0000 Subject: [PATCH 011/107] =?UTF-8?q?i18n:=20Language=20update=20from=20Ling?= =?UTF-8?q?oHub=20=F0=9F=A4=96=20on=202022-09-05Z=20(#26805)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../rocketchat-i18n/i18n/de.i18n.json | 17 +- .../rocketchat-i18n/i18n/it.i18n.json | 12 +- .../rocketchat-i18n/i18n/pl.i18n.json | 315 +++++++++++++++++- 3 files changed, 337 insertions(+), 7 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index b99451a2a23..dbf2cc5b21c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -3,6 +3,8 @@ "500": "Interner Serverfehler", "__count__empty_rooms_will_be_removed_automatically": "__count__ leere Räume werden automatisch entfernt.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ leere Räume werden automatisch entfernt:<br/> __rooms__.", + "__count__message_pruned": "__count__ Nachricht gelöscht", + "__count__message_pruned_plural": "__count__ Nachrichten gelöscht", "__usersCount__members_joined": "+ __usersCount__ Mitglieder beigetreten", "__usersCount__people_will_be_invited": "__usersCount__ Mitglieder werden eingeladen", "__username__is_no_longer__role__defined_by__user_by_": "__username__ ist nicht länger __role__, geändert durch __user_by__", @@ -284,6 +286,8 @@ "add-livechat-department-agents_description": "Berechtigung zum Hinzufügen von Omnichannel-Agenten zu Abteilungen", "add-oauth-service": "OAuth-Dienst hinzufügen", "add-oauth-service_description": "Berechtigung, einen neuen OAuth-Dienst hinzuzufügen", + "add-team-channel": "Team Channel hinzufügen", + "add-team-member": "Teammitglied hinzufügen", "add-user": "Benutzer erstellen", "add-user_description": "Berechtigung, über den Benutzer-Bildschirm neue Nutzer anzulegen", "add-user-to-any-c-room": "Benutzer zu jedem öffentlichen Channel hinzufügen", @@ -321,6 +325,7 @@ "Agent_deactivated": "Agent wurde deaktiviert", "Agent_Without_Extensions": "Agent ohne Erweiterungen", "Agents": "Agenten", + "Agree": "Zustimmen", "Alerts": "Benachrichtigungen", "Alias": "Alias", "Alias_Format": "Alias-Format", @@ -443,6 +448,7 @@ "App_status_manually_disabled": "Deaktiviert: Manuell", "App_status_manually_enabled": "Aktiviert", "App_status_unknown": "Unbekannt", + "App_Store": "App Store", "App_support_url": "Support-URL", "App_Url_to_Install_From": "Von URL installieren", "App_Url_to_Install_From_File": "Aus Datei installieren", @@ -769,6 +775,8 @@ "Caller_Id": "Anrufer-ID", "Cam_on": "Kamera an", "Cam_off": "Kamera aus", + "can-audit": "Kann auditieren", + "can-audit-log": "Kann Log auditieren", "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", "Canceled": "Abgebrochen", @@ -1062,6 +1070,7 @@ "Content": "Inhalt", "Continue": "Weiter", "Continuous_sound_notifications_for_new_livechat_room": "Durchgehende Soundbenachrichtigungen für den neuen Livechat-Room", + "convert-team": "Team konvertieren", "Conversation": "Gespräch", "Conversation_closed": "Gespräch geschlossen: __comment__.", "Conversation_closing_tags": "Tags bei Schließen der Konversation", @@ -1331,6 +1340,7 @@ "Create": "Erstellen", "Create_Canned_Response": "Vorformulierte Antwort erstellen", "Create_channel": "Channel erstellen", + "Create_channels": "Channels erstellen", "Create_A_New_Channel": "Neuen Channel erstellen", "Create_new": "Neu erstellen", "Create_new_members": "Neue Mitglieder erstellen", @@ -1420,6 +1430,7 @@ "CustomSoundsFilesystem_Description": "Legen Sie fest, wie benutzerdefinierte Sounds gespeichert werden.", "Daily_Active_Users": "Täglich aktive Benutzer", "Dashboard": "Dashboard", + "Data_modified": "Daten verändert", "Data_processing_consent_text": "Einwilligung in die Datenverarbeitung", "Data_processing_consent_text_description": "Verwenden Sie diese Einstellung, um zu erläutern, dass Sie während des Gesprächs persönliche Informationen des Kunden sammeln, speichern und verarbeiten können.", "Date": "Datum", @@ -1474,6 +1485,7 @@ "delete-own-message_description": "Berechtigung eigene Nachrichten zu löschen", "delete-p": "Private Channels löschen", "delete-p_description": "Berechtigung, private Channels zu löschen", + "delete-team": "Team löschen", "delete-user": "Benutzer löschen", "delete-user_description": "Berechtigung, einen Benutzer zu löschen", "Deleted": "Gelöscht!", @@ -1487,6 +1499,7 @@ "Deployment": "Implementierung", "Description": "Beschreibung", "Desktop": "Desktop", + "Desktop_apps": "Desktopapplikationen", "Desktop_Notification_Test": "Desktop-Benachrichtigungstest", "Desktop_Notifications": "Desktop-Benachrichtigungen", "Desktop_Notifications_Default_Alert": "Desktop-Benachrichtigungen bei", @@ -1654,6 +1667,8 @@ "edit-other-user-totp_description": "Berechtigung zum Bearbeiten des Zwei-Faktor-TOTP eines anderen Benutzers", "edit-privileged-setting": "Besonders geschützte Einstellungen ändern", "edit-privileged-setting_description": "Berechtigung, besonders geschützte Einstellungen zu ändern", + "edit-team": "Team bearbeiten", + "edit-team-channel": "Team Channel editieren", "edit-room": "Room bearbeiten", "edit-room_description": "Berechtigung, einen Room zu bearbeiten (Name, Thema, Sichtbarkeit, Archivierung)", "edit-room-avatar": "Room-Avatar bearbeiten", @@ -2561,7 +2576,7 @@ "Keyboard_Shortcuts_Move_To_End_Of_Message": "Ans Ende der Nachricht springen", "Keyboard_Shortcuts_New_Line_In_Message": "Zeilenumbruch einfügen", "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "Channel öffnen / Nach Benutzer suchen", - "Keyboard_Shortcuts_Title": "Tastatürkürzel", + "Keyboard_Shortcuts_Title": "Tastaturkürzel", "Knowledge_Base": "Wissensdatenbank", "Label": "Bezeichnung", "Language": "Sprache", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index 4e659c3e99f..6aa36396317 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -3,6 +3,8 @@ "500": "Errore interno del server", "__count__empty_rooms_will_be_removed_automatically": "__count__ stanze vuote verranno automaticamente rimosse.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ stanze vuote saranno rimosse automaticamente:<br/> __rooms__.", + "__count__message_pruned": "__count__ messaggio eliminato", + "__count__message_pruned_plural": "__count__ messaggi eliminati", "__usersCount__members_joined": "+ __usersCount__ membri si sono uniti", "__usersCount__people_will_be_invited": "__usersCount__ persone saranno invitate", "__username__is_no_longer__role__defined_by__user_by_": "__username__ non è più __role__, da __user_by__", @@ -2213,10 +2215,10 @@ "Room_archivation_state_false": "Attivo", "Room_archivation_state_true": "Archiviati", "Room_archived": "Canale archiviato", - "room_changed_announcement": "Annuncio del canale cambiato in: __room_announcement__ da __user_by__", - "room_changed_description": "Descrizione del canale cambiata in: __room_description__ da __user_by__", - "room_changed_privacy": "Tipo di canale cambiato in: __room_type__ da __user_by__", - "room_changed_topic": "Argomento del canale cambiato in: __room_topic__ da __user_by__", + "room_changed_announcement": "annuncio cambiato in: __room_announcement__ by __user_by__", + "room_changed_description": "descrizione cambiata in: __room_description__ by __user_by__", + "room_changed_privacy": "tipo cambiato in: __room_type__ by __user_by__", + "room_changed_topic": "argomento cambiato in: __room_topic__ by __user_by__", "Room_default_change_to_private_will_be_default_no_more": "Questo è un canale predefinito e cambiandolo a un gruppo privato non causerà più un canale predefinito. Vuoi procedere?", "Room_description_changed_successfully": "Descrizione del canale cambiata con successo", "Room_has_been_archived": "La stanza è stata archiviata", @@ -2226,7 +2228,7 @@ "room_is_blocked": "La stanza è sbloccata", "room_is_read_only": "Questo canale è in sola lettura", "room_name": "nome canale", - "Room_name_changed": "Il nome del canale è stato cambiato in: __room_name__ da __user_by__", + "Room_name_changed": "nome cambiato in: __room_name__ by __user_by__", "Room_name_changed_successfully": "Nome del canale cambiato con successo", "Room_not_found": "Canale non trovato", "Room_password_changed_successfully": "Password della stanza cambiata con successo", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 58852027a54..7785e22ad44 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -3,6 +3,10 @@ "500": "WewnÄ™trzny bÅ‚Ä…d serwera", "__count__empty_rooms_will_be_removed_automatically": "Puste pokoje (__count__) zostanÄ… automatycznie usuniÄ™te.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "Puste pokoje (__count__) zostanÄ… automatycznie usuniÄ™te:<br/> __rooms__.", + "__count__message_pruned": "__count__ wiadomość(i) usuniÄ™tych", + "__count__message_pruned_plural": "__count__ wiadomość(i) usuniÄ™tych", + "__usersCount__members_joined": "+ __usersCount__ czÅ‚onków doÅ‚Ä…czyÅ‚o", + "__usersCount__people_will_be_invited": "__usersCount__ ludzi zostanie zostanie zaproszonych", "__username__is_no_longer__role__defined_by__user_by_": "Użytkownik __username__ nie ma już roli __role__; zmieniÅ‚ to użytkownik __user_by__", "__username__was_set__role__by__user_by_": "Użytkownik __username__ otrzymaÅ‚ rolÄ™ __role__ od użytkownika __user_by__", "This_room_encryption_has_been_enabled_by__username_": "Użytkownik __username__ wÅ‚Ä…czyÅ‚ szyfrowanie w tym pokoju", @@ -34,6 +38,7 @@ "access-setting-permissions_description": "Uprawnienie do modyfikacji uprawnieÅ„ opartych na ustawieniach", "Accessing_permissions": "Uzyskiwanie dostÄ™pu do uprawnieÅ„", "Account_SID": "SID konta", + "Account": "Konto", "Accounts": "Konta", "Accounts_Description": "Zmodyfikuj ustawienia konta czÅ‚onka obszaru roboczego", "Accounts_Admin_Email_Approval_Needed_Default": "<p>Użytkownik <b>[name] ([email])</b> zostaÅ‚ zarejestrowany.</p><p>Sprawdź „Administracja -> Użytkownicyâ€, aby aktywować lub usunąć.</p>", @@ -69,6 +74,7 @@ "Accounts_CustomFieldsToShowInUserInfo": "Pola niestandardowe wyÅ›wietlane w informacjach o użytkowniku", "Accounts_Default_User_Preferences": "DomyÅ›lne preferencje użytkownika", "Accounts_Default_User_Preferences_audioNotifications": "DomyÅ›lny alert powiadomieÅ„ dźwiÄ™kowych", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Umożliwienie użytkownikom wybrania opcji \"wyÅ›lij do kanaÅ‚u\"", "Accounts_Default_User_Preferences_desktopNotifications": "DomyÅ›lny alert powiadomieÅ„ z pulpitu", "Accounts_Default_User_Preferences_pushNotifications": "DomyÅ›lny alert powiadomieÅ„ push", "Accounts_Default_User_Preferences_not_available": "Nie udaÅ‚o siÄ™ pobrać preferencji użytkownika, ponieważ nie zostaÅ‚y jeszcze skonfigurowane przez użytkownika", @@ -252,6 +258,7 @@ "Accounts_UserAddedEmail_Default": "<h1>Witamy w <strong>[Site_Name]</strong></h1><p>Przejdź na stronÄ™ <a href=\"[Site_URL]\">[Site_URL]</a> i wypróbuj dziÅ› najlepsze dostÄ™pne otwarte rozwiÄ…zanie czatu!</p><p> Możesz zalogować siÄ™ przy użyciu adresu e-mail: [email] i hasÅ‚a: [password]. Może wystÄ…pić konieczność zmiany hasÅ‚a po pierwszym logowaniu.", "Accounts_UserAddedEmail_Description": "Możesz użyć znaczników: <br/><ul><li>[name], [fname], [lname], aby wstawić odpowiednio peÅ‚nÄ… nazwÄ™ użytkownika, jego imiÄ™ lub nazwisko.</li><li>[email], aby wstawić adres e-mail użytkownika.</li><li>[password], aby wstawić hasÅ‚o użytkownika.</li><li>[Site_Name] i [Site_URL], aby wstawić odpowiednio nazwÄ™ aplikacji i adres URL.</li></ul>", "Accounts_UserAddedEmailSubject_Default": "Dodano CiÄ™ do [Site_Name]", + "Accounts_Verify_Email_For_External_Accounts": "Oznaczaj Email dla kont zewnÄ™trznych jako zweryfikowany", "Action": "Akcja", "Action_required": "Wymagana akcja", "Activate": "Aktywuj", @@ -269,15 +276,21 @@ "Add_Reaction": "Dodaj reakcjÄ™", "Add_Role": "Dodaj rolÄ™", "Add_Sender_To_ReplyTo": "Dodaj nadawcÄ™ do odbiorców odpowiedzi", + "Add_URL": "Dodaj URL", "Add_user": "Dodaj użytkownika", "Add_User": "Dodaj użytkownika", "Add_users": "Dodaj użytkowników", "Add_members": "Dodaj czÅ‚onków", "add-all-to-room": "Dodaj wszystkich użytkowników do pokoju.", + "add-all-to-room_description": "Uprawnienie do dodawania wszystkich użytkowników do pokoju", "add-livechat-department-agents": "Dodaj agentów Omnichannel do działów", "add-livechat-department-agents_description": "Uprawnienie do dodawania agentów omnichannel do działów", "add-oauth-service": "Dodaj usÅ‚ugÄ™ Oauth", "add-oauth-service_description": "Uprawnienie do dodawania nowej usÅ‚ugi Oauth", + "add-team-channel": "Dodaj zespół Channel", + "add-team-channel_description": "Zezwolenie na dodanie kanaÅ‚u do zespoÅ‚u", + "add-team-member": "Dodaj czÅ‚onka zespoÅ‚u", + "add-team-member_description": "Uprawnienie do dodawania czÅ‚onków do zespoÅ‚u", "add-user": "Dodaj użytkownika", "add-user_description": "Uprawnienie do dodawania nowych użytkowników do serwera na ekranie użytkowników", "add-user-to-any-c-room": "Dodaj użytkownika do dowolnego publicznego kanaÅ‚u Channel", @@ -297,7 +310,11 @@ "additional_integrations_Zapier": "Czy chcesz zintegrować inne oprogramowanie i aplikacje z Rocket.Chat, ale nie masz czasu, aby zrobić to rÄ™cznie? Proponujemy użycie narzÄ™dzia Zapier, które w peÅ‚ni obsÅ‚ugujemy. Przeczytaj wiÄ™cej na ten temat w naszej dokumentacji. <a href=\"https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/\" target=\"_blank\">https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/</a>", "Admin_disabled_encryption": "Administrator nie wÅ‚Ä…czyÅ‚ szyfrowania E2E", "Admin_Info": "Informacje administracyjne", + "admin-no-active-video-conf-provider": "**PoÅ‚Ä…czenie konferencyjne nie jest wÅ‚Ä…czone**: Skonfiguruj poÅ‚Ä…czenia konferencyjne, aby byÅ‚y dostÄ™pne w tej przestrzeni roboczej.", + "admin-video-conf-provider-not-configured": "**PoÅ‚Ä…czenie konferencyjne nie jest wÅ‚Ä…czone**: Skonfiguruj poÅ‚Ä…czenia konferencyjne, aby byÅ‚y dostÄ™pne w tej przestrzeni roboczej.", + "admin-no-videoconf-provider-app": "**PoÅ‚Ä…czenie konferencyjne nie jest wÅ‚Ä…czone**: Aplikacje do poÅ‚Ä…czeÅ„ konferencyjnych sÄ… dostÄ™pne w marketplace Rocket.Chat.", "Administration": "Administracja", + "Address": "Adres", "Adult_images_are_not_allowed": "Obrazy dla dorosÅ‚ych nie sÄ… dozwolone", "Aerospace_and_Defense": "PrzemysÅ‚ lotniczy i obronny", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Po uwierzytelnieniu OAuth2 użytkownicy bÄ™dÄ… przekierowywani na adres URL z tej listy. W każdym wierszu można dodać jeden adres URL.", @@ -311,6 +328,7 @@ "Agent_deactivated": "Agent zostaÅ‚ zdezaktywowany", "Agent_Without_Extensions": "Agent bez rozszerzeÅ„", "Agents": "Agenci", + "Agree": "Zgadza siÄ™", "Alerts": "Alerty", "Alias": "Alias", "Alias_Format": "Format aliasu", @@ -325,6 +343,8 @@ "All_closed_chats_have_been_removed": "Wszystkie zamkniÄ™te czaty zostaÅ‚y usuniÄ™te", "All_logs": "Wszystkie dzienniki", "All_messages": "Wszystkie wiadomoÅ›ci", + "All_Prices": "Wszystkie ceny", + "All_status": "CaÅ‚y status", "All_users": "Wszyscy użytkownicy", "All_users_in_the_channel_can_write_new_messages": "Wszyscy użytkownicy na kanale mogÄ… pisać nowe wiadomoÅ›ci", "Allow_collect_and_store_HTTP_header_informations": "Zezwalaj na zbieranie i przechowywanie danych nagłówków HTTP", @@ -338,6 +358,7 @@ "Allow_switching_departments": "Zezwalaj odwiedzajÄ…cym na przeÅ‚Ä…czanie działów", "Almost_done": "Prawie gotowe", "Alphabetical": "Alfabetycznie", + "Also_send_thread_message_to_channel_behavior": "WysyÅ‚aj wiadomoÅ›ci z wÄ…tku również do kanaÅ‚u", "Also_send_to_channel": "WyÅ›lij także do kanaÅ‚u", "Always_open_in_new_window": "Zawsze otwieraj w nowym oknie", "Analytics": "Analityka", @@ -430,6 +451,7 @@ "App_status_manually_disabled": "WyÅ‚Ä…czone: rÄ™cznie", "App_status_manually_enabled": "WÅ‚Ä…czone", "App_status_unknown": "Nieznane", + "App_Store": "App Store", "App_support_url": "adres URL pomocy", "App_Url_to_Install_From": "Zainstaluj z adresu URL", "App_Url_to_Install_From_File": "Zainstaluj z pliku", @@ -588,6 +610,7 @@ "Audio_Notifications_Value": "DomyÅ›lny dźwiÄ™k powiadomienia o wiadomoÅ›ci", "Audios": "DźwiÄ™ki", "Auditing": "Audyt", + "Auth": "Auth", "Auth_Token": "Token uwierzytelniajÄ…cy", "Authentication": "Uwierzytelnianie", "Author": "Autor", @@ -661,12 +684,14 @@ "BBB_Start_Meeting": "Rozpocznij spotkanie", "BBB_Video_Call": "Wideokonferencja BBB", "BBB_You_have_no_permission_to_start_a_call": "Nie masz uprawnienia do rozpoczÄ™cia poÅ‚Ä…czenia", + "Be_the_first_to_join": "BÄ…dź pierwszym, który doÅ‚Ä…czy", "Belongs_To": "Należy do", "Best_first_response_time": "Najlepszy czas pierwszej odpowiedzi", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Funkcja beta. Wymaga wÅ‚Ä…czenia wideokonferencji.", "Better": "Lepszy", "Bio": "Biografia", "Bio_Placeholder": "Symbol zastÄ™pczy biografii", + "Block": "Blokuj", "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Liczba nieudanych prób do momentu zablokowania wg IP", "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Liczba nieudanych prób do momentu zablokowania wg użytkownika", "Block_Multiple_Failed_Logins_By_Ip": "Blokuj nieudane próby logowania wg IP", @@ -682,6 +707,10 @@ "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "Tutaj bÄ™dÄ… odbierane powiadomienia. Upewnij siÄ™, że kanaÅ‚ istnieje. Nazwa kanaÅ‚u nie powinna zawierać symbolu #", "Block_User": "Zablokuj użytkownika", "Blockchain": "Blockchain", + "block-ip-device-management": "Blokowanie IP do zarzÄ…dzania urzÄ…dzeniami", + "block-ip-device-management_description": "Zezwolenie na zablokowanie adresu IP", + "Block_IP_Address": "Blokowanie adresu IP", + "Blocked_IP_Addresses": "Zablokowane adresy IP", "Blockstack": "Blockstack", "Blockstack_Description": "Daj czÅ‚onkom obszaru roboczego możliwość logowania siÄ™ bez polegania na stronach trzecich lub serwerach zdalnych.", "Blockstack_Auth_Description": "Opis autora", @@ -723,6 +752,7 @@ "Business_hours_updated": "Godziny pracy zaktualizowane", "busy": "zajÄ™ty", "Busy": "ZajÄ™ty", + "Buy": "Kup", "By": "Autor:", "by": "autor:", "By_author": "Autor: __author__", @@ -731,6 +761,8 @@ "Calling": "Dzwoni", "Call_Center": "Centrum telefoniczne", "Call_Center_Description": "Konfiguracja Rocket.Chat call center.", + "Call_ended": "PoÅ‚Ä…czenie zakoÅ„czone", + "Calls": "PoÅ‚Ä…czenia", "Calls_in_queue": "PoÅ‚Ä…czeÅ„ w kolejce: __calls__", "Calls_in_queue_plural": "__calls__ poÅ‚Ä…czeÅ„ w kolejce", "Calls_in_queue_empty": "Kolejka jest pusta", @@ -738,10 +770,19 @@ "Call_Information": "Informacje o poÅ‚Ä…czeniu", "Call_provider": "Dostawca poÅ‚Ä…czenia", "Call_Already_Ended": "PoÅ‚Ä…czenie już zakoÅ„czone", + "Call_number": "Numer poÅ‚Ä…czenia", + "Call_number_enterprise_only": "Numer poÅ‚Ä…czenia (tylko wersja Enterprise Edition)", "call-management": "ZarzÄ…dzanie poÅ‚Ä…czeniami", "call-management_description": "Zezwolenie na rozpoczÄ™cie spotkania", + "Call_unavailable_for_federation": "PoÅ‚Ä…czenie jest niedostÄ™pne dla pokoi Federacji", "Caller": "DzwoniÄ…cy", "Caller_Id": "ID dzwoniÄ…cego", + "Cam_on": "Kamera wÅ‚Ä…czona", + "Cam_off": "Kamera wyÅ‚Ä…czona", + "can-audit": "Możliwość Audit", + "can-audit_description": "Pozwolenie na dostÄ™p do audytu", + "can-audit-log": "Możliwość Audit Log", + "can-audit-log_description": "Uprawnienie do dostÄ™pu do dziennika audytu", "Cancel": "Anuluj", "Cancel_message_input": "Anuluj", "Canceled": "Anulowano", @@ -888,11 +929,13 @@ "Check_All": "Zaznacz wszystko", "Check_if_the_spelling_is_correct": "Sprawdź, czy pisownia jest poprawna", "Check_Progress": "Sprawdź postÄ™p", + "Check_device_activity": "Sprawdź aktywność urzÄ…dzenia", "Choose_a_room": "Wybierz pokój", "Choose_messages": "Wybierz wiadomoÅ›ci", "Choose_the_alias_that_will_appear_before_the_username_in_messages": "Wybierz alias, który pojawi siÄ™ przed nazwÄ… użytkownika w wiadomoÅ›ciach.", "Choose_the_username_that_this_integration_will_post_as": "Wybierz nazwÄ™ użytkownika, pod którÄ… ta integracja bÄ™dzie publikować.", "Choose_users": "Wybierz użytkowników", + "Clean_History_unavailable_for_federation": "Czyszczenie historii jest niedostÄ™pne dla federacji", "Clean_Usernames": "Wyczyść nazwy użytkowników", "clean-channel-history": "Wyczyść historiÄ™ kanaÅ‚u", "clean-channel-history_description": "Uprawnienie do usuwania historii z kanałów", @@ -903,6 +946,7 @@ "clear_history": "Wyczyść historiÄ™", "Clear_livechat_session_when_chat_ended": "Wyczyść sesjÄ™ goÅ›cia po zakoÅ„czeniu czatu", "clear-oembed-cache": "Wyczyść cache OEmbed", + "clear-oembed-cache_description": "Pozwolenie na wyczyszczenie pamiÄ™ci podrÄ™cznej OEmbed", "Click_here": "Kliknij tutaj", "Click_here_for_more_details_or_contact_sales_for_a_new_license": "<a target=\"_blank\" href=\"__url__\">Kliknij tutaj</a>, aby uzyskać wiÄ™cej informacji, lub napisz na adres <strong>__email__</strong> w celu uzyskania nowej licencji.", "Click_here_for_more_info": "Kliknij tutaj, aby uzyskać wiÄ™cej informacji", @@ -914,6 +958,7 @@ "Click_to_load": "Kliknij, aby zaÅ‚adować", "Client_ID": "ID klienta", "Client_Secret": "Tajny klucz klienta", + "Client": "Klient", "Clients_will_refresh_in_a_few_seconds": "Klienci zostanÄ… odÅ›wieżeni w ciÄ…gu kilku sekund", "close": "zamknij", "Close": "Zamknij", @@ -986,7 +1031,9 @@ "Comment_to_leave_on_closing_session": "Komentarz do zamkniÄ™cia sesji", "Comment": "Komentarz", "Common_Access": "Wspólny dostÄ™p", + "Commit": "Commit", "Community": "SpoÅ‚eczność", + "Free_Edition": "Edycja darmowa", "Compact": "Kompaktowy", "Composer_not_available_phone_calls": "WiadomoÅ›ci nie sÄ… dostÄ™pne w przypadku poÅ‚Ä…czeÅ„ telefonicznych", "Condensed": "Skondensowany", @@ -994,6 +1041,8 @@ "Commit_details": "Szczegóły zatwierdzenia", "Completed": "UkoÅ„czone", "Computer": "Komputer", + "Conference_call_has_ended": "_Call has ended._", + "Conference_name": "Nazwa konferencji", "Configure_Incoming_Mail_IMAP": "Konfiguracja poczty przychodzÄ…cej (IMAP)", "Configure_Outgoing_Mail_SMTP": "Konfiguracja poczty wychodzÄ…cej (SMTP)", "Confirm": "Potwierdź", @@ -1002,6 +1051,8 @@ "Confirm_New_Password_Placeholder": "Wprowadź ponownie nowe hasÅ‚o…", "Confirm_password": "Potwierdź hasÅ‚o", "Confirmation": "Potwierdzenie", + "Configure_video_conference": "Konfiguracja poÅ‚Ä…czenia konferencyjnego", + "Configure_video_conference_to_use": "Skonfiguruj poÅ‚Ä…czenia konferencyjne, aby byÅ‚y dostÄ™pne w tej przestrzeni roboczej.", "Connect": "PoÅ‚Ä…cz", "Connected": "PoÅ‚Ä…czony", "Connect_SSL_TLS": "PoÅ‚Ä…cz siÄ™ za pomocÄ… SSL/TLS", @@ -1026,6 +1077,8 @@ "Content": "Zawartość", "Continue": "Kontynuuj", "Continuous_sound_notifications_for_new_livechat_room": "CiÄ…gÅ‚e powiadomienia dźwiÄ™kowe dla nowego pokoju omnichannel", + "convert-team": "Konwertuj zespół", + "convert-team_description": "Zezwolenie na przeksztaÅ‚cenie zespoÅ‚u w kanaÅ‚", "Conversation": "Rozmowa", "Conversation_closed": "Rozmowa zamkniÄ™ta: __comment__.", "Conversation_closing_tags": "Znaczniki zamykajÄ…ce rozmowy", @@ -1295,6 +1348,8 @@ "Create": "Utwórz", "Create_Canned_Response": "Utwórz predefiniowanÄ… odpowiedź", "Create_channel": "Utwórz kanaÅ‚", + "Create_channels": "Tworzenie kanałów", + "Create_a_public_channel_that_new_workspace_members_can_join": "Utwórz publiczny kanaÅ‚, do którego mogÄ… doÅ‚Ä…czyć nowi czÅ‚onkowie obszaru roboczego.", "Create_A_New_Channel": "Utwórz nowy kanaÅ‚", "Create_new": "Utwórz nowy", "Create_new_members": "Utwórz nowych czÅ‚onków", @@ -1310,6 +1365,7 @@ "create-personal-access-tokens": "Utwórz osobiste tokeny dostÄ™pu", "create-personal-access-tokens_description": "Uprawnienia do tworzenia osobistych tokenów dostÄ™pu", "create-team": "Utwórz zespół", + "create-team_description": "Uprawnienia do tworzenia zespołów", "create-user": "Utwórz użytkownika", "create-user_description": "Uprawnienie do tworzenia użytkowników", "Created": "Utworzony", @@ -1345,6 +1401,8 @@ "Custom_Field_Removed": "Pole niestandardowe usuniÄ™te", "Custom_Field_Not_Found": "Nie znaleziono pola niestandardowego", "Custom_Integration": "Integracja niestandardowa", + "Custom_OAuth_has_been_added": "Dodano niestandardowy OAuth", + "Custom_OAuth_has_been_removed": "Niestandardowy OAuth zostaÅ‚ usuniÄ™ty", "Custom_oauth_helper": "Podczas konfigurowania dostawcy OAuth należy podać adres URL wywoÅ‚ania zwrotnego. Użyj adresu <pre>%s</pre>.", "Custom_oauth_unique_name": "Niestandardowa unikatowa nazwa oauth", "Custom_Script_Logged_In": "Skrypt niestandardowy dla zalogowanych użytkowników", @@ -1382,6 +1440,7 @@ "CustomSoundsFilesystem_Description": "OkreÅ›l jak wÅ‚asne dźwiÄ™ki sÄ… przechowywane", "Daily_Active_Users": "Dzienna liczba aktywnych użytkowników", "Dashboard": "Panel", + "Data_modified": "Dane Zmienione", "Data_processing_consent_text": "Tekst zgody na przetwarzanie danych", "Data_processing_consent_text_description": "Użyj tego ustawienia, aby wyjaÅ›nić, że możesz zbierać, przechowywać i przetwarzać dane osobowe klienta podczas rozmowy.", "Date": "Data", @@ -1411,6 +1470,7 @@ "Deactivate": "Dezaktywuj", "Decline": "Odrzuć", "Decode_Key": "Klucz dekodowania", + "default": "DomyÅ›lny", "Default": "DomyÅ›lny", "Default_value": "DomyÅ›lna wartość", "Delete": "UsuÅ„", @@ -1435,6 +1495,8 @@ "delete-own-message_description": "Uprawnienie do usuwania wÅ‚asnej wiadomoÅ›ci", "delete-p": "UsuÅ„ kanaÅ‚y prywatne", "delete-p_description": "Uprawnienie do usuwania kanałów prywatnych", + "delete-team": "Zespół domyÅ›lny", + "delete-team_description": "Zezwolenie na usuwanie zespołów", "delete-user": "UsuÅ„ użytkownika", "delete-user_description": "Uprawnienie do usuwania użytkowników", "Deleted": "UsuniÄ™to!", @@ -1448,6 +1510,7 @@ "Deployment": "Wdrożenie", "Description": "Opis", "Desktop": "Pulpit", + "Desktop_apps": "Aplikacje Desktop", "Desktop_Notification_Test": "Test powiadomienia na pulpicie", "Desktop_Notifications": "Powiadomienia na pulpicie", "Desktop_Notifications_Default_Alert": "DomyÅ›lny alert powiadomieÅ„ na pulpicie", @@ -1457,8 +1520,21 @@ "Desktop_Notifications_Enabled": "Powiadomienia na pulpicie sÄ… wÅ‚Ä…czone", "Desktop_Notifications_Not_Enabled": "Powiadomienia na pulpicie nie sÄ… wÅ‚Ä…czone", "Details": "Szczegóły", + "Device_Changes_Not_Available": "Zmiany w urzÄ…dzeniach niedostÄ™pne w tej przeglÄ…darce. Aby uzyskać gwarancjÄ™ dostÄ™pnoÅ›ci, skorzystaj z oficjalnej aplikacji desktopowej Rocket.Chat.", + "Device_Changes_Not_Available_Insecure_Context": "Zmiany w urzÄ…dzeniach sÄ… dostÄ™pne tylko w bezpiecznych kontekstach (np. https://)", + "Device_Management": "ZarzÄ…dzanie urzÄ…dzeniami", + "Device_ID": "Identyfikator urzÄ…dzenia", + "Device_Info": "Informacje o urzÄ…dzeniu", + "Device_Logged_Out": "UrzÄ…dzenie wylogowane", + "Device_Logout_Text": "UrzÄ…dzenie zostanie wylogowane z przestrzeni roboczej, a bieżąca sesja zostanie zakoÅ„czona. Użytkownik bÄ™dzie mógÅ‚ zalogować siÄ™ ponownie przy użyciu tego samego urzÄ…dzenia.", + "Devices": "UrzÄ…dzenia", + "Devices_Set": "Zestaw urzÄ…dzeÅ„", + "Device_settings": "Ustawienia urzÄ…dzenia", + "Dialed_number_doesnt_exist": "Wybrany numer nie istnieje", + "Dialed_number_is_incomplete": "Wybrany numer nie jest kompletny", "Different_Style_For_User_Mentions": "Inny styl dla wzmianek użytkowników", "Direct": "BezpoÅ›redni", + "Direction": "Kierunek", "Direct_Message": "Wiadomość bezpoÅ›rednia", "Direct_message_creation_description": "Zamierzasz utworzyć czat z wieloma użytkownikami. Dodaj tych, z którymi chcesz rozmawiać (wszyscy w tym samym miejscu), używajÄ…c wiadomoÅ›ci bezpoÅ›rednich.", "Direct_message_someone": "WyÅ›lij wiadomość bezpoÅ›redniÄ…", @@ -1494,6 +1570,7 @@ "Disallow_reacting_Description": "Nie zezwala na reagowanie", "Discard": "Odrzuć", "Disconnect": "RozÅ‚Ä…cz", + "Discover_public_channels_and_teams_in_the_workspace_directory": "Odkryj publiczne kanaÅ‚y i zespoÅ‚y w katalogu przestrzeni roboczej.", "Discussion": "Dyskusja", "Discussion_Description": "Dyskusje to dodatkowy sposób organizowania konwersacji, który umożliwia zapraszanie użytkowników z kanałów zewnÄ™trznych do udziaÅ‚u w okreÅ›lonych konwersacjach.", "Discussion_description": "BÄ…dź na bieżąco z tym co siÄ™ dzieje! Stwórz dyskusjÄ™ czyli doÅ‚Ä…cz sub-kanaÅ‚ do tego kanaÅ‚u.", @@ -1505,6 +1582,7 @@ "Discussion_target_channel_description": "Wybierz kanaÅ‚, który jest powiÄ…zany z tym, o co chcesz zapytać", "Discussion_target_channel_prefix": "Tworzysz dyskusjÄ™ w", "Discussion_title": "Stwórz nowÄ… dyskusjÄ™", + "Discussions_unavailable_for_federation": "Dyskusje sÄ… niedostÄ™pne dla kanałów federacji", "discussion-created": "__message__", "Discussions": "Dyskusje", "Display": "WyÅ›wietlanie", @@ -1523,6 +1601,7 @@ "Do_you_have_any_notes_for_this_conversation": "Czy masz jakieÅ› notatki do tej rozmowy?", "Do_you_want_to_accept": "Czy chcesz zaakceptować?", "Do_you_want_to_change_to_s_question": "Czy chcesz zmienić na <strong>%s</strong>?", + "Documentation": "Dokumentacja", "Document_Domain": "Domena dokumentu", "Domain": "Domena", "Domain_added": "domena dodana", @@ -1533,6 +1612,7 @@ "Dont_ask_me_again": "Nie pytaj mnie ponownie!", "Dont_ask_me_again_list": "Nie pytaj mnie jeszcze raz", "Download": "Pobierz", + "Download_Destkop_App": "Pobierz aplikacjÄ™ desktop", "Download_Info": "Pobierz informacje", "Download_My_Data": "Pobierz moje dane", "Download_Pending_Avatars": "Pobierz oczekujÄ…ce awatary", @@ -1574,6 +1654,7 @@ "Edit_Canned_Responses": "Edycja predefiniowanej odpowiedzi", "Edit_Custom_Field": "Edycja niestandardowego pola", "Edit_Department": "Edytuj oddziaÅ‚", + "Edit_Federated_User_Not_Allowed": "Brak możliwoÅ›ci edycji użytkownika sfederowanego", "Edit_Invite": "Edytuj zaproszenie", "Edit_previous_message": "`%s` - Edytuj poprzedniÄ… wiadomość", "Edit_Priority": "Edytuj priorytet", @@ -1600,6 +1681,12 @@ "edit-other-user-totp_description": "Uprawnienie do edycji funkcji Two Factor TOTP innego użytkownika", "edit-privileged-setting": "Edytuj ustawienia uprzywilejowane", "edit-privileged-setting_description": "Zezwolenie na edycjÄ™ ustawieÅ„", + "edit-team": "Edycja zespoÅ‚u", + "edit-team_description": "Pozwolenie na edycjÄ™ zespołów", + "edit-team-channel": "Edycja zespoÅ‚u Channel", + "edit-team-channel_description": "Uprawnienia do edycji kanaÅ‚u zespoÅ‚u", + "edit-team-member": "Edycja czÅ‚onka zespoÅ‚u", + "edit-team-member_description": "Uprawnienia do edycji czÅ‚onków zespoÅ‚u", "edit-room": "Edytuj pokój", "edit-room_description": "Zezwolenie na edycjÄ™ nazwy pokoju, tematu, typu (status prywatny lub publiczny) i statusu (aktywny lub zarchiwizowany)", "edit-room-avatar": "Edytuj avatar pokoju", @@ -1647,7 +1734,10 @@ "Emoji_provided_by_JoyPixels": "Emotikony dostarcza <strong>JoyPixels</strong>", "EmojiCustomFilesystem": "System plików niestandardowych emotikonów", "EmojiCustomFilesystem_Description": "OkreÅ›l jak przechowywane sÄ… emoji.", + "Empty_no_agent_selected": "Puste, nie wybrano żadnego agenta", "Empty_title": "Pusty tytuÅ‚", + "Use_Legacy_Message_Template": "Użycie starego szablonu wiadomoÅ›ci", + "Use_Legacy_Message_Template_alert": "To jest przestarzaÅ‚a funkcja. Może nie dziaÅ‚ać zgodnie z oczekiwaniami i nie bÄ™dzie otrzymywać nowych aktualizacji", "Enable": "WÅ‚Ä…cz", "Enable_Auto_Away": "WÅ‚Ä…cz Auto Away", "Enable_CSP": "WÅ‚Ä…cz politykÄ™ bezpieczeÅ„stwa treÅ›ci (Content-Security-Policy)", @@ -1670,6 +1760,7 @@ "Encryption_key_saved_successfully": "Twój klucz szyfrowania zostaÅ‚ zapisany pomyÅ›lnie.", "EncryptionKey_Change_Disabled": "Nie można ustawić hasÅ‚a dla klucza szyfrowania, ponieważ klucz prywatny nie jest obecny na tym kliencie. Aby ustawić nowe hasÅ‚o, musisz zaÅ‚adować swój klucz prywatny przy użyciu istniejÄ…cego hasÅ‚a lub użyć klienta, w którym klucz jest już zaÅ‚adowany.", "End": "Koniec", + "End_suspicious_sessions": "ZakoÅ„cz wszelkie podejrzane sesje", "End_call": "Koniec poÅ‚Ä…czenia", "Expand_view": "RozwiÅ„ widok", "Explore_marketplace": "PrzeglÄ…daj Marketplace", @@ -1724,6 +1815,7 @@ "error-blocked-username": "<strong>__field__</strong> jest zablokowane i nie może być użyte!", "error-canned-response-not-found": "Predefiniowana odpowiedź nie odnaleziona", "error-cannot-delete-app-user": "Usuwanie użytkownika aplikacyjnego jest niedozwolone, odinstaluj odpowiedniÄ… aplikacjÄ™ aby móc go usunąć.", + "error-cant-add-federated-users": "Nie można dodać użytkowników federacyjnych do pokoju niefederacyjnego", "error-cant-invite-for-direct-room": "Nie można zaprosić użytkownika do bezpoÅ›rednich pokoi", "error-channels-setdefault-is-same": "Ustawienie domyÅ›lne kanaÅ‚u jest takie samo jak to, do którego zostanie zmienione.", "error-channels-setdefault-missing-default-param": "Parametr bodyParam \"default\" jest wymagany", @@ -1836,6 +1928,7 @@ "error-tags-must-be-assigned-before-closing-chat": "Tag(i) musi (muszÄ…) być przypisane przed zamkniÄ™ciem czatu", "error-the-field-is-required": "Wymagane jest __field__ pola.", "error-this-is-not-a-livechat-room": "To nie jest pokój Livechata", + "error-this-is-an-ee-feature": "To jest funkcja edycji Enterprise", "error-token-already-exists": "Token o tej nazwie już istnieje", "error-token-does-not-exists": "Token nie istnieje", "error-too-many-requests": "BÅ‚Ä…d, zbyt wiele żądaÅ„. ProszÄ™ zwolnij. Musisz czekać __seconds__ sekund przed ponownÄ… próbÄ….", @@ -1854,6 +1947,7 @@ "error-no-permission-team-channel": "Nie masz uprawnieÅ„, aby dodać ten kanaÅ‚ do zespoÅ‚u", "error-no-owner-channel": "Tylko wÅ‚aÅ›ciciele mogÄ… dodać ten kanaÅ‚ do zespoÅ‚u", "error-you-are-last-owner": "JesteÅ› ostatnim wÅ‚aÅ›cicielem. ProszÄ™ ustaw nowego przed opuszczeniem pokoju.", + "You_do_not_have_permission_to_do_this": "Nie masz na to pozwolenia", "Errors_and_Warnings": "BÅ‚Ä™dy i ostrzeżenia", "Esc_to": "NaciÅ›nij Esc: ", "Estimated_due_time": "Szacowany czas (w minutach)", @@ -1909,12 +2003,14 @@ "Failed_To_Load_Import_Operation": "Nie udaÅ‚o siÄ™ zaÅ‚adować operacji importu", "Failed_To_Start_Import": "Nie udaÅ‚o siÄ™ rozpocząć operacji importu", "Failed_to_validate_invite_token": "Nie udaÅ‚o siÄ™ zwalidować tokenu zaproszenia", + "Failure": "Niepowodzenie", "False": "Nie", "Fallback_forward_department": "DziaÅ‚ awaryjny dla przekierowaÅ„", "Fallback_forward_department_description": "Umożliwia zdefiniowanie dziaÅ‚u awaryjnego, który bÄ™dzie odbieraÅ‚ czaty przekazywane do tego dziaÅ‚u w przypadku, gdy w danej chwili nie ma agentów online.", "Favorite": "Ulubiony", "Favorite_Rooms": "WÅ‚Ä…cz ulubione pokoje", "Favorites": "Ulubione", + "featured": "wyróżniony", "Featured": "Wyróżniony", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Ta funkcja zależy od wyżej wybranego dostawcy poÅ‚Ä…czenia wÅ‚Ä…czonego z ustawieÅ„ administracyjnych.<br/>Dla **Jitsi**, proszÄ™ upewnić siÄ™, że masz Jitsi Enabled w Admin -> Video Conference -> Jitsi -> Enabled.<br/>Dla **WebRTC**, proszÄ™ upewnić siÄ™, że masz WebRTC wÅ‚Ä…czone w Admin -> WebRTC -> Enabled.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Ta funkcja zależy od wÅ‚Ä…czenia \"WyÅ›lij historiÄ™ nawigacji użytkownika jako wiadomość\".", @@ -1954,6 +2050,7 @@ "Federation_Public_key": "Klucz publiczny", "Federation_Public_key_details": "To jest klucz, którym musisz siÄ™ podzielić z innymi. Do czego to sÅ‚uży?", "Federation_Search_users_you_want_to_connect": "Wyszukaj użytkownika, z którym chcesz siÄ™ poÅ‚Ä…czyć, używajÄ…c kombinacji nazwy użytkownika i domeny lub adresu e-mail, np:", + "Federation_slash_commands": "Polecenia Federacji", "Federation_SRV_no_support": "JeÅ›li Twój dostawca DNS nie obsÅ‚uguje rekordów SRV z _http lub _https", "Federation_SRV_no_support_details": "Niektórzy dostawcy DNS nie pozwalajÄ… na ustawienie _https lub _http w rekordach SRV, wiÄ™c mamy wsparcie dla tych przypadków, używajÄ…c naszej starej metody rozwiÄ…zywania rekordów DNS.", "Federation_SRV_records_200": "Rekord SRV (2.0.0 lub nowszy)", @@ -1982,6 +2079,9 @@ "Federation_Matrix": "Federacja V2", "Federation_Matrix_enabled": "WÅ‚Ä…czone", "Federation_Matrix_Enabled_Alert": "Wsparcie Federacji Matrix jest w wersji alfa. Stosowanie w systemie produkcyjnym nie jest obecnie zalecane.<a target=\"_blank\" href=\"https://go.rocket.chat/i/matrix-federation\">WiÄ™cej informacji na temat obsÅ‚ugi Matrix Federation można znaleźć tutaj</>", + "Federation_Matrix_Federated": "Sfederowany", + "Federation_Matrix_Federated_Description": "TworzÄ…c pokój federacyjny nie bÄ™dziesz mógÅ‚ wÅ‚Ä…czyć szyfrowania ani rozgÅ‚aszania", + "Federation_Matrix_Federated_Description_disabled": "Ta funkcja jest obecnie wyÅ‚Ä…czona w tym obszarze roboczym.", "Federation_Matrix_id": "AppService ID", "Federation_Matrix_hs_token": "Token serwera domowego", "Federation_Matrix_as_token": "Token AppService", @@ -2014,6 +2114,7 @@ "files": "akta", "Files": "Pliki", "Files_only": "Usuwaj tylko zaÅ‚Ä…czone pliki, zachowaj wiadomoÅ›ci", + "Files_unavailable_for_federation": "Pliki sÄ… niedostÄ™pne dla kanałów sfederowanych", "files_pruned": "pliki zostaÅ‚y przyciÄ™te", "FileSize_Bytes": "__fileSize__ Bajty", "FileSize_KB": "__fileSize__ KB", @@ -2022,6 +2123,7 @@ "FileUpload_Description": "Skonfiguruj przesyÅ‚anie i przechowywanie plików.", "FileUpload_Cannot_preview_file": "Nie można wyÅ›wietlić podglÄ…du pliku", "FileUpload_Disabled": "PrzesyÅ‚anie plików jest wyÅ‚Ä…czone.", + "FileUpload_Disabled_for_federation": "PrzesyÅ‚anie plików jest wyÅ‚Ä…czone dla kanałów sfederowanych.", "FileUpload_Enable_json_web_token_for_files": "WÅ‚Ä…czenie Json Web Tokens protection do przesyÅ‚ania plików", "FileUpload_Enable_json_web_token_for_files_description": "Dodaje JWT do adresów url wysyÅ‚anych plików", "FileUpload_Enabled": "PrzesyÅ‚anie plików wÅ‚Ä…czone", @@ -2080,7 +2182,9 @@ "FileUpload_Webdav_Username": "Nazwa użytkownika WebDAV", "Filter": "Filtr", "Filter_by_category": "Filtrowanie wedÅ‚ug kategorii", + "Filter_by_Custom_Fields": "Filtrowanie wedÅ‚ug pól niestandardowych", "Filter_By_Price": "Filtruj wg ceny", + "Filter_By_Status": "Filtrowanie wedÅ‚ug statusu", "Filters": "Filtry", "Filters_applied": "Zastosowane filtry", "Financial_Services": "UsÅ‚ugi finansowe", @@ -2148,6 +2252,7 @@ "get-password-policy-mustContainAtLeastOneSpecialCharacter": "HasÅ‚o powinno zawierać co najmniej jeden znak specjalny", "get-password-policy-mustContainAtLeastOneUppercase": "HasÅ‚o powinno zawierać co najmniej jednÄ… wielkÄ… literÄ™", "get-server-info": "Pobierz info o serwerze", + "get-server-info_description": "Pozwolenie na uzyskanie informacji o serwerze", "github_no_public_email": "Nie posiadasz publicznego konta e-mail przypisanego do swojego profilu GitHub.", "github_HEAD": "HEAD", "Give_a_unique_name_for_the_custom_oauth": "Podaj unikalnÄ… nazwÄ™ dla wÅ‚asnego OAuth", @@ -2157,13 +2262,16 @@ "Global_purge_override_warning": "ObowiÄ…zuje globalna polityka przechowywania. JeÅ›li zostawisz opcjÄ™ \"ZastÄ…p globalnÄ… zasadÄ™ przechowywania\", możesz zastosować tylko politykÄ™ bardziej rygorystycznÄ… niż polityka globalna.", "Global_Search": "Wyszukiwanie globalne", "Go_to_your_workspace": "Idź do swojej przestrzeni roboczej", + "Google_Play": "Google Play", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Plik JSON klucza usÅ‚ugi. WiÄ™cej informacji można znaleźć [tutaj] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Menedżer tagów Google Id", + "Got_it": "Mam to", "Government": "RzÄ…d", "Graphql_CORS": "GraphQL CORS", "Graphql_Enabled": "WÅ‚Ä…czono GraphQL", "Graphql_Subscription_Port": "Port subskrypcji GraphQL", + "Grid_view": "Widok siatki", "Group": "Grupa", "Group_by": "Grupuj wedÅ‚ug", "Group_by_Type": "Grupuj wedÅ‚ug typu", @@ -2203,7 +2311,9 @@ "History": "Historia", "Hold_Time": "Czas zawieszenia", "Hold_Call": "ZawieÅ› poÅ‚Ä…czenie", + "Hold_Call_EE_only": "Zawieszenie poÅ‚Ä…czenia (tylko wersja Enterprise)", "Home": "Dom", + "Homepage": "Strona główna", "Host": "Host", "Hospitality_Businness": "Biznes szpitalny", "hours": "godzin", @@ -2242,6 +2352,7 @@ "Iframe_X_Frame_Options_Description": "Opcje do X-Frame-Options. [Możesz zobaczyć wszystkie opcje tutaj.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)", "Ignore": "Ignorować", "Ignored": "Ignorowane", + "Ignore_Two_Factor_Authentication": "Ignorowanie uwierzytelniania dwuskÅ‚adnikowego", "Images": "Obrazki", "IMAP_intercepter_already_running": "Intercom protokoÅ‚u IMAP już dziaÅ‚a", "IMAP_intercepter_Not_running": "Intercom protokoÅ‚u IMAP Nie dziaÅ‚a", @@ -2304,10 +2415,13 @@ "Importing_users": "Importowanie użytkowników", "Inactivity_Time": "Czas nieaktywnoÅ›ci", "In_progress": "W toku", + "inbound-voip-calls": "PrzychodzÄ…ce poÅ‚Ä…czenia Voip", + "inbound-voip-calls_description": "Pozwolenie na przychodzÄ…ce poÅ‚Ä…czenia voip", "Inbox_Info": "Informacje o skrzynce odbiorczej", "Include_Offline_Agents": "UwzglÄ™dnij agentów bÄ™dÄ…cych offline", "Inclusive": "WÅ‚Ä…cznie", "Incoming": "PrzychodzÄ…cy", + "Incoming_call_from": "PoÅ‚Ä…czenie przychodzÄ…ce z", "Incoming_Livechats": "PrzychodzÄ…cy LIvechat", "Incoming_WebHook": "PrzychodzÄ…cy WebHook", "Industry": "Typ dziaÅ‚alnoÅ›ci", @@ -2411,13 +2525,16 @@ "Invitation_Subject_Default": "ZostaÅ‚eÅ› zaproszony do [Site_Name]", "Invite": "Zaproszenie", "Invites": "Zaproszenia", + "Invite_and_add_members_to_this_workspace_to_start_communicating": "ZaproÅ› i dodaj czÅ‚onków do tej przestrzeni roboczej, aby rozpocząć komunikacjÄ™.", "Invite_Link": "Link zaproszenia", + "Invite_link_generated": "Link zapraszajÄ…cy zostaÅ‚ wygenerowany", "Invite_removed": "Zaproszenie zostaÅ‚o usuniÄ™te", "Invite_user_to_join_channel": "ZaproÅ› użytkownika by doÅ‚Ä…czyÅ‚ do kanaÅ‚u", "Invite_user_to_join_channel_all_from": "ZaproÅ› wszystkich użytkowników z [#channel], aby doÅ‚Ä…czyć do tego kanaÅ‚u", "Invite_user_to_join_channel_all_to": "ZaproÅ› wszystkich użytkowników tego kanaÅ‚u do przyÅ‚Ä…czenia siÄ™ [kanaÅ‚ #]", "Invite_Users": "ZaproÅ› użytkowników", "IP": "IP", + "IP_Address": "Adres IP", "IRC_Channel_Join": "WyjÅ›cie polecenia JOIN.", "IRC_Channel_Leave": "Wynik polecenia PART.", "IRC_Channel_Users": "WyjÅ›cie polecenia NAMES.", @@ -2449,14 +2566,17 @@ "Join_audio_call": "DoÅ‚Ä…cz do rozmowy audio", "Join_call": "DoÅ‚Ä…cz do poÅ‚Ä…czenia", "Join_Chat": "DoÅ‚Ä…cz do czatu", + "Join_conference": "DoÅ‚Ä…cz do konferencji", "Join_default_channels": "DoÅ‚Ä…cz do domyÅ›lnych kanałów", "Join_the_Community": "DoÅ‚Ä…cz do spoÅ‚ecznoÅ›ci", "Join_the_given_channel": "DoÅ‚Ä…cz do tego kanaÅ‚u", + "Join_rooms": "DoÅ‚Ä…cz do pokoii", "Join_video_call": "DoÅ‚Ä…cz do poÅ‚Ä…czenia wideo", "Join_my_room_to_start_the_video_call": "DoÅ‚Ä…cz do mojego pokoju, aby rozpocząć rozmowÄ™ wideo", "join-without-join-code": "DoÅ‚Ä…cz bez kodu doÅ‚Ä…czania", "join-without-join-code_description": "Zezwolenie na ominiÄ™cie kodu doÅ‚Ä…czania w kanaÅ‚ach z wÅ‚Ä…czonym kodem Å‚Ä…czenia", "Joined": "DoÅ‚Ä…czyÅ‚", + "joined": "doÅ‚Ä…czono", "Joined_at": "DoÅ‚Ä…czyÅ‚ o", "JSON": "JSON", "Jump": "Skocz", @@ -2537,6 +2657,8 @@ "Layout_Login_Terms": "Regulamin rejestacji", "Layout_Privacy_Policy": "Polityka PrywatnoÅ›ci", "Layout_Show_Home_Button": "Pokaż przycisk \"Strona domowa\"", + "Layout_Custom_Body_Only": "Pokaż tylko zawartość niestandardowÄ…", + "Layout_Custom_Body_Only_description": "Ukryj domyÅ›lne karty strony głównej.", "Layout_Sidenav_Footer": "Stopka panelu nawigacyjnego", "Layout_Sidenav_Footer_description": "Stopka ma rozmiar 260 x 70 pikseli", "Layout_Terms_of_Service": "Regulamin", @@ -2688,7 +2810,9 @@ "LDAP_Validate_Teams_For_Each_Login_Description": "OkreÅ›l, czy zespoÅ‚y użytkowników powinny być aktualizowane za każdym razem, gdy zalogujÄ… siÄ™ do Rocket.Chat. JeÅ›li ta opcja jest wyÅ‚Ä…czona, zespół bÄ™dzie Å‚adowany tylko przy pierwszym logowaniu.", "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", + "Learn_more": "Dowiedz siÄ™ wiÄ™cej", "Least_recent_updated": "Najstarsza aktualizacja", + "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Dowiedz siÄ™, jak odblokować niezliczone możliwoÅ›ci Rocket.Chat.", "Leave": "Opuść", "Leave_a_comment": "Zostaw komentarz", "Leave_Group_Warning": "Czy na pewno chcesz opuÅ›cić grupÄ™ \"%s\"?", @@ -2712,6 +2836,7 @@ "List_of_departments_for_forward_description": "Zezwalaj na ustawienie zastrzeżonej listy departamentów, które mogÄ… otrzymywać czaty z tego departamentu.", "List_of_departments_to_apply_this_business_hour": "Lista departamentów w których ma być zastosowana ta lista godzin pracy", "List_of_Direct_Messages": "Lista wiadomoÅ›ci bezpoÅ›rednich", + "List_view": "Widok listy", "Livechat": "Livechat", "Livechat_abandoned_rooms_action": "Jak obsÅ‚użyć porzucanie goÅ›cia", "Livechat_abandoned_rooms_closed_custom_message": "Niestandardowy komunikat, gdy pokój jest automatycznie zamykany przez brak aktywnoÅ›ci odwiedzajÄ…cego", @@ -2797,6 +2922,7 @@ "Livestream_enable_audio_only": "WÅ‚Ä…cz tylko tryb audio", "Livestream_enabled": "Livestream wÅ‚Ä…czony", "Livestream_not_found": "Transmisja na żywo nie jest dostÄ™pna", + "Livestream_unavailable_for_federation": "Livestram jest niedostÄ™pny dla pokoi Federated", "Livestream_popout": "Otwórz transmisjÄ™ na żywo", "Livestream_source_changed_succesfully": "ŹródÅ‚o transmisji zostaÅ‚o pomyÅ›lnie zmienione", "Livestream_switch_to_room": "Przejdź na transmisjÄ™ z bieżącego pokoju", @@ -2829,6 +2955,7 @@ "Log_Trace_Subscriptions_Filter": "Åšledź filtr subskrypcji", "Log_Trace_Subscriptions_Filter_Description": "Tekst tutaj zostanie oceniony jako RegExp (`new RegExp ('text')`). Pozostaw to puste, aby pokazać Å›lad każdego poÅ‚Ä…czenia.", "Log_View_Limit": "Limit przeglÄ…dania logów", + "Logged_Out_Banner_Text": "Twój administrator przestrzeni roboczej zakoÅ„czyÅ‚ sesjÄ™ na tym urzÄ…dzeniu. Zaloguj siÄ™ ponownie, aby kontynuować.", "Logged_out_of_other_clients_successfully": "Wylogowanie z innymi klientów powiodÅ‚o siÄ™", "Login": "Login", "Login_Attempts": "Nieudane próby logowania", @@ -2842,6 +2969,12 @@ "Logistics": "Logistyka", "Logout": "Wyloguj", "Logout_Others": "Wyloguj z innych zalogowanych urzÄ…dzeÅ„", + "Logout_Device": "Wyloguj urzÄ…dzenie", + "Log_out_devices_remotely": "Zdalne wylogowanie urzÄ…dzeÅ„", + "logout-device-management": "ZarzÄ…dzanie wylogowywaniem urzÄ…dzeÅ„", + "logout-device-management_description": "Uprawnienie do wylogowania innych użytkowników z pulpitu zarzÄ…dzania urzÄ…dzeniami", + "logout-other-user": "Wyloguj innego użytkownika", + "logout-other-user_description": "Uprawnienie do wylogowania innych użytkowników", "Logs": "Logi", "Logs_Description": "Skonfiguruj sposób odbierania logów serwera", "Longest_chat_duration": "NajdÅ‚uższy czas trwania rozmowy", @@ -2863,12 +2996,15 @@ "Make_Admin": "Przydziel Admina", "Make_sure_you_have_a_copy_of_your_codes_1": "Upewnij siÄ™, że masz kopiÄ™ kodów:", "Make_sure_you_have_a_copy_of_your_codes_2": "JeÅ›li utracisz dostÄ™p do aplikacji uwierzytelniajÄ…cej, możesz zalogować siÄ™ przy użyciu jednego z tych kodów.", + "manage-agent-extension-association": "ZarzÄ…dzaj asocjacjÄ… rozszerzeÅ„ agenta", + "manage-agent-extension-association_description": "Uprawnienie do zarzÄ…dzania asocjacjÄ… rozszerzeÅ„ agenta", "manage-apps": "ZarzÄ…dzać aplikacjami", "manage-apps_description": "Uprawnienia do zarzÄ…dzania wszystkimi aplikacjami", "manage-assets": "ZarzÄ…dzaj aktywami", "manage-assets_description": "Zezwolenie na zarzÄ…dzanie zasobami serwera", "manage-cloud": "ZarzÄ…dzaj chmurÄ…", "manage-cloud_description": "ZarzÄ…dzaj chmurÄ…", + "Manage_Devices": "ZarzÄ…dzaj urzÄ…dzeniami", "manage-email-inbox": "ZarzÄ…dzaj skrzynkÄ… odbiorczÄ… Email", "manage-email-inbox_description": "Uprawnienia do zarzÄ…dzania skrzynkami Email", "manage-emoji": "ZarzÄ…dzaj emotikonami", @@ -2880,10 +3016,20 @@ "manage-integrations_description": "Zezwolenie na zarzÄ…dzanie integracjami serwerów", "manage-livechat-agents": "ZarzÄ…daj agentami Omnichannel", "manage-livechat-agents_description": "Uprawnienia do zarzÄ…dzania agentami omnichannel", + "manage-livechat-canned-responses": "ZarzÄ…daj predefiniowanymi odpowiedziami dla Omnichannel", + "manage-livechat-canned-responses_description": "Uprawnienia do zarzÄ…dzania predefiniowanymi odpowiedziami dla Omnichannel", "manage-livechat-departments": "ZarzÄ…daj departamentami Omnichannel", "manage-livechat-departments_description": "Uprawnienia do zarzÄ…dzania departamentami omnichannel", "manage-livechat-managers": "ZarzÄ…daj managerami Omnichannel", "manage-livechat-managers_description": "Uprawnienia do zarzÄ…dzania menedżerami omnichannel", + "manage-livechat-monitors": "ZarzÄ…dzaj monitorami Omnichannel", + "manage-livechat-monitors_description": "Uprawnienia do zarzÄ…dzania monitorami omnichannel", + "manage-livechat-priorities": "ZarzÄ…dzaj priorytetami omnichannel", + "manage-livechat-priorities_description": "Pozwolenie na zarzÄ…dzanie priorytetami omnichannel", + "manage-livechat-tags": "ZarzÄ…dzaj Tagami Omnichannel ", + "manage-livechat-tags_description": "Uprawnienia do zarzÄ…dzania tagami omnichannel", + "manage-livechat-units": "ZarzÄ…dzaj jednostkami omnichannel", + "manage-livechat-units_description": "Uprawnienia do zarzÄ…dzania jednostkami omnichannel", "manage-oauth-apps": "ZarzÄ…dzaj aplikacjami Oauth", "manage-oauth-apps_description": "Zezwolenie na zarzÄ…dzanie aplikacjami Oauth na serwerze", "manage-outgoing-integrations": "ZarzÄ…daj integracjami wychodzÄ…cymi", @@ -2901,6 +3047,10 @@ "manage-the-app": "ZarzÄ…dzaj aplikacjÄ…", "manage-user-status": "ZarzÄ…dzaj statusem użytkownika", "manage-user-status_description": "Zezwolenie na zarzÄ…dzanie niestandardowymi statusami użytkowników serwera", + "manage-voip-call-settings": "ZarzÄ…dzaj ustawieniami poÅ‚Ä…czeÅ„ Voip", + "manage-voip-call-settings_description": "Uprawnienia do zarzÄ…dzania ustawieniami poÅ‚Ä…czeÅ„ voip", + "manage-voip-contact-center-settings": "ZarzÄ…dzaj ustawieniami Voip Contact Center", + "manage-voip-contact-center-settings_description": "Uprawnienia do zarzÄ…dzania ustawieniami voip contact center", "Manager_added": "Menedżer dodany", "Manager_removed": "Menedżer usuniÄ™ty", "Managers": "Menedżerowie", @@ -2940,6 +3090,7 @@ "Max_number_of_chats_per_agent": "Maksymalna liczba jednoczesnych rozmów", "Max_number_of_chats_per_agent_description": "Maksymalna liczba jednoczestnych rozmów w których mogÄ… uczestniczyć agenci", "Max_number_of_uses": "Maksymalna liczba użyć", + "Max_Retry": "Maksymalna ilość prób ponownego poÅ‚Ä…czenia z serwerem", "Maximum": "Maksimum", "Maximum_number_of_guests_reached": "Maksymalna liczba osiÄ…gniÄ™tych goÅ›ci", "Me": "Ja", @@ -2952,6 +3103,7 @@ "mention-here": "Wzmianka @here", "mention-here_description": "Zezwolenie na użycie wzmianki @here", "Mentions": "Wzmianki o tobie", + "Mentions_unavailable_for_federation": "Wzmianki sÄ… niedostÄ™pne dla pokoi skonfederowanych", "Mentions_default": "Wzmianki (domyÅ›lnie)", "Mentions_only": "Tylko wzmianki", "Merge_Channels": "Scal kanaÅ‚y", @@ -3105,6 +3257,8 @@ "meteor_status_try_now_waiting": "Spróbuj teraz", "meteor_status_waiting": "Poczekaj na poÅ‚Ä…czenie serwera", "Method": "Metoda", + "Mic_on": "Mikrofon wÅ‚Ä…czony", + "Microphone": "Mikrofon", "Mic_off": "Mikrofon wyÅ‚Ä…czony", "Min_length_is": "Minimalna dÅ‚ugość to %s", "Minimum": "Minimum", @@ -3122,8 +3276,10 @@ "Mobex_sms_gateway_restful_address_desc": "IP lub Host Twojego Mobex REST API. Np. `http://192.168.1.1:8080` lub `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Nazwa użytkownika", "Mobile": "Powiadomnienia mobilne", + "Mobile_apps": "Aplikacje mobilne", "Mobile_Description": "Zdefiniuj zachowania dotyczÄ…ce Å‚Ä…czenia siÄ™ z obszarem roboczym z urzÄ…dzeÅ„ mobilnych.", "mobile-upload-file": "Zezwalaj na przesyÅ‚anie plików na urzÄ…dzeniach mobilnych", + "mobile-upload-file_description": "Zezwolenie na przesyÅ‚anie plików na urzÄ…dzeniach mobilnych", "Mobile_Push_Notifications_Default_Alert": "DomyÅ›lne powiadomienia mobilne", "Monday": "PoniedziaÅ‚ek", "Mongo_storageEngine": "Silnik Mongo", @@ -3132,6 +3288,7 @@ "MongoDB_Deprecated": "PrzestarzaÅ‚e MongoDB", "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "Wersja MongoDB %s jest przestarzaÅ‚a. PodnieÅ› wersjÄ™ swojej bazy.", "Monitor_added": "Monitor dodany", + "Monitor_new_and_suspicious_logins": "Monitorowanie nowych i podejrzanych logowaÅ„", "Monitor_history_for_changes_on": "Sprawdź historiÄ™ zmian na", "Monitor_removed": "Monitor usuniÄ™ty", "Monitors": "Monitory", @@ -3150,7 +3307,9 @@ "Msgs": "WiadomoÅ›ci", "multi": "multi", "multi_line": "linia multi", + "Multiple_monolith_instances_alert": "ObsÅ‚ugujesz wiele instancji... niektóre funkcje nie bÄ™dÄ… zachowywać siÄ™ zgodnie z projektem.", "Mute": "Wyciszenie", + "Mute_and_dismiss": "Wycisz i odrzuć", "Mute_all_notifications": "Wycisz wszystkie powiadomienia", "Mute_Focused_Conversations": "Wycisz skoncentruj siÄ™ na konwersacje", "Mute_Group_Mentions": "Wycisz wzmianki @all i @here", @@ -3177,6 +3336,8 @@ "New_Application": "Nowa aplikacja", "New_Business_Hour": "Nowe godziny pracy", "New_Canned_Response": "Nowa predefiniowana odpowiedź", + "New_Call": "Nowe poÅ‚Ä…czenie", + "New_Call_Enterprise_Edition_Only": "Nowe poÅ‚Ä…czenie (tylko wersja Enterprise)", "New_chat_in_queue": "Nowa rozmowa w kolejce", "New_chat_priority": "Zmieniono priorytet: __user__ zmieniÅ‚ priorytet na __priority__", "New_chat_transfer": "Nowy transfer czatu: __transfer__", @@ -3213,6 +3374,7 @@ "Nickname": "Pseudonim", "Nickname_Placeholder": "Podaj swój pseudonim...", "No": "Nie", + "no-active-video-conf-provider": "**PoÅ‚Ä…czenie konferencyjne nie jest wÅ‚Ä…czone**: Administrator obszaru roboczego musi najpierw wÅ‚Ä…czyć funkcjÄ™ poÅ‚Ä…czenia konferencyjnego.", "No_available_agents_to_transfer": "Brak dostÄ™pnych agentów do przeniesienia", "No_app_matches": "Å»adna aplikacja nie pasuje", "No_app_matches_for": "Brak pasujÄ…cych aplikacji dla", @@ -3231,6 +3393,7 @@ "No_files_found": "Nie znaleziono plików", "No_files_left_to_download": "Brak plików do pobrania", "No_groups_yet": "Nie masz prywatnych grup.", + "No_history": "Nie ma historii", "No_installed_app_matches": "Å»adna zainstalowana aplikacja nie pasuje", "No_integration_found": "Nie znaleziono integracji za pomocÄ… podanego identyfikatora.", "No_Limit": "Brak limitu", @@ -3249,6 +3412,7 @@ "No_starred_messages": "Brak ulubionych wiadomoÅ›ci", "No_such_command": "Brak komendy `/__command__`", "No_Threads": "Nie znaleziono wÄ…tków", + "no-videoconf-provider-app": "**PoÅ‚Ä…czenie konferencyjne nie jest dostÄ™pne**: Aplikacje do poÅ‚Ä…czeÅ„ konferencyjnych mogÄ… być instalowane w marketplace Rocket.Chat przez administratora przestrzeni roboczej.", "Nobody_available": "Nikt nie dostÄ™pny", "Node_version": "Wersja node'a", "None": "Å»aden", @@ -3343,6 +3507,10 @@ "Omnichannel_External_Frame_Encryption_JWK_Description": "JeÅ›li podane, szyfruje on token użytkownika dostarczonym kluczem, a zewnÄ™trzny system bÄ™dzie musiaÅ‚ odszyfrować dane, aby uzyskać dostÄ™p do tokena", "Omnichannel_External_Frame_URL": "Adres URL zewnÄ™trznej ramki", "On": "Na", + "on-hold-livechat-room": "Wstrzymaj Omnichannel RoomRoom", + "on-hold-livechat-room_description": "Pozwolenie na wstrzymanie omnichannel roomu", + "on-hold-others-livechat-room": "Wstrzymaj inne Omnichannel Room", + "on-hold-others-livechat-room_description": "Pozwolenie na wstrzymanie innych kanałów omnichannel", "On_Hold": "Wstrzymano", "On_Hold_Chats": "On Hold", "On_Hold_conversations": "Rozmowy w trakcie oczekiwania", @@ -3356,15 +3524,21 @@ "Only_works_with_chrome_version_greater_50": "DziaÅ‚a tylko z przeglÄ…darkami Chrome w wersjach > 50", "Only_you_can_see_this_message": "Tylko Ty widzisz tÄ™ wiadomość", "Only_invited_users_can_acess_this_channel": "Tylko zaproszeni użytkownicy mogÄ… mieć dostÄ™p do tego kanaÅ‚u", + "Only_available_on_Enterprise_learn_more__URL": "DostÄ™pne tylko w wersji Enterprise. [Dowiedz siÄ™ wiÄ™cej](__URL__)", "Oops_page_not_found": "Ups, strona nie zostaÅ‚a znaleziona", "Oops!": "Ups", "Open": "Otwarty", + "Open_call": "Otwórz poÅ‚Ä…czenie", + "Open_call_in_new_tab": "Otwórz poÅ‚Ä…czenie w nowej karcie", "Open_channel_user_search": "`%s` - Otwórz kanaÅ‚ / wyszukiwanie użytkownika", "Open_conversations": "Otwarte rozmowy", "Open_Days": "Dni otwarte", "Open_days_of_the_week": "Otwarte dni tygodnia", + "Open_Dialpad": "Otwórz Dialpad", + "Open_directory": "Otwórz katalog", "Open_Livechats": "Otwarte Livechaty", "Open_menu": "Open_menu", + "Open_settings": "Otwórz ustawienia", "Open_thread": "Otwórz wÄ…tek", "Open_your_authentication_app_and_enter_the_code": "Otwórz aplikacjÄ™ uwierzytelniajÄ…cÄ… i wprowadź kod. Możesz również użyć jednego z kodów zapasowych.", "Opened": "Otworzony", @@ -3381,6 +3555,7 @@ "Organization_Name": "Nazwa organizacji", "Organization_Type": "Typ Organizacji", "Original": "Oryginalny", + "OS": "OS", "OS_Arch": "Architektura systemu", "OS_Cpus": "Ilość procesorów", "OS_Freemem": "Ilość wolnej pamiÄ™ci systemu", @@ -3404,6 +3579,8 @@ "OTR_Enable_Description": "WÅ‚Ä…cz tÄ™ opcjÄ™, aby używać wiadomoÅ›ci nierejestrowanych (OTR) w wiadomoÅ›ciach bezpoÅ›rednich miÄ™dzy 2 użytkownikami. WiadomoÅ›ci OTR nie sÄ… rejestrowane na serwerze. SÄ… szyfrowane i wymieniane bezpoÅ›rednio miÄ™dzy 2 użytkownikami.", "OTR_message": "Wiadomość OTR", "OTR_is_only_available_when_both_users_are_online": "OTR jest dostÄ™pna tylko wtedy, gdy obaj użytkownicy sÄ… online", + "outbound-voip-calls": "PoÅ‚Ä…czenia wychodzÄ…ce Voip", + "outbound-voip-calls_description": "Zezwolenie na poÅ‚Ä…czenia wychodzÄ…ce voip", "Out_of_seats": "Brak miejsc", "Outgoing": "WychodzÄ…cy", "Outgoing_WebHook": "WychodzÄ…cy WebHook", @@ -3412,6 +3589,8 @@ "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "ZastÄ…p adres URL, do którego sÄ… przesyÅ‚ane pliki. Ten url również wykorzystywane do pobrania, chyba że podano CDN", "Owner": "WÅ‚aÅ›ciciel", "Play": "Odtwórz", + "Page_not_exist_or_not_permission": "Strona nie istnieje lub nie masz uprawnieÅ„ dostÄ™pu", + "Page_not_found": "Nie znaleziono strony", "Page_title": "TytuÅ‚ strony", "Page_URL": "URL strony", "Pages": "Strony", @@ -3454,6 +3633,7 @@ "pin-message_description": "Zezwolenie na przypiÄ™cie wiadomoÅ›ci na kanale", "Pinned_a_message": "PrzypiÄ™ta wiadomość:", "Pinned_Messages": "PrzypiÄ™te wiadomoÅ›ci", + "Pinned_messages_unavailable_for_federation": "WiadomoÅ›ci przypiÄ™te nie sÄ… dostÄ™pne dla pokoi sfederowanych.", "pinning-not-allowed": "Przypinanie niedozwolone", "PiwikAdditionalTrackers": "Dodatkowe witryny Piwik", "PiwikAdditionalTrackers_Description": "Wprowadź dodatkowe adresy URL stron internetowych Piwik i SiteID w nastÄ™pujÄ…cym formacie, jeÅ›li chcesz Å›ledzić te same dane w różnych witrynach: [{ \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }{ \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 }, ]", @@ -3468,6 +3648,9 @@ "Placeholder_for_email_or_username_login_field": "Placeholder dla pola e-maila lub pola nazwy użytkownika (przy logowaniu)", "Placeholder_for_password_login_confirm_field": "Potwierdź placeholder dla niepowodzenia logowania hasÅ‚em", "Placeholder_for_password_login_field": "Placeholder dla hasÅ‚a (przy logowaniu)", + "Platform_Windows": "Windows", + "Platform_Linux": "Linux", + "Platform_Mac": "Mac", "Please_add_a_comment": "ProszÄ™ dodać komentarz", "Please_add_a_comment_to_close_the_room": "ProszÄ™ dodać komentarz, aby zamknąć pokój", "Please_answer_survey": "ProszÄ™ poÅ›więć chwilÄ™, aby odpowiedzieć na krótkÄ… ankietÄ™ na temat tej rozmowy", @@ -3492,6 +3675,7 @@ "Please_wait_while_OTR_is_being_established": "Poczekaj, rozmowa OTR jest nawiÄ…zywana", "Please_wait_while_your_account_is_being_deleted": "Poczekaj, konto jest usuwane...", "Please_wait_while_your_profile_is_being_saved": "Poczekaj, Twój profil jest zapisywany...", + "Policies": "Polityka", "Pool": "SMTP Pool", "Port": "Port", "Post_as": "Pisz wiadomoÅ›ci jako", @@ -3500,6 +3684,7 @@ "Post_to_s_as_s": "Pisz do <strong>%s</strong> jako <strong>%s</strong>", "post-readonly": "Pisanie w kanale tylko do oczytu", "post-readonly_description": "Zezwolenie na opublikowanie wiadomoÅ›ci w kanale tylko do odczytu", + "Powered_by_RocketChat": "Powered by Rocket.Chat", "Preferences": "Preferencje", "Preferences_saved": "Preferencje zapisane", "Preparing_data_for_import_process": "Przygotowywanie danych do procesu importu", @@ -3518,6 +3703,8 @@ "Priority_removed": "Priorytet usuniÄ™ty", "Privacy": "Prywatność", "Privacy_Policy": "Polityka PrywatnoÅ›ci", + "Privacy_policy": "Polityka PrywatnoÅ›ci", + "Privacy_summary": "Podsumowanie dotyczÄ…ce prywatnoÅ›ci", "Private": "Prywatny", "Private_Channel": "Prywatny kanaÅ‚", "Private_Channels": "KanaÅ‚y prywatne", @@ -3608,6 +3795,7 @@ "Reason_To_Join": "Powód do przyÅ‚Ä…czenia siÄ™", "Receive_alerts": "Otrzymuj powiadomienia", "Receive_Group_Mentions": "Otrzymuj wspomnienia @all i @here", + "Receive_login_notifications": "Otrzymywanie powiadomieÅ„ o logowaniu", "Recent_Import_History": "Ostatnia historia importu", "Record": "Nagrywaj", "recording": "Nagrywania", @@ -3636,11 +3824,14 @@ "Register_Server_Standalone_Update_Settings": "Zaktualizuj wstÄ™pnie skonfigurowane ustawienia", "Register_Server_Terms_Alert": "ProszÄ™ zaakceptować warunki aby ukoÅ„czyć rejestracjÄ™", "register-on-cloud": "Zarejestruj w chmurze", + "register-on-cloud_description": "Zezwolenie na rejestracjÄ™ w chmurze", "Registration": "Rejestracja", "Registration_Succeeded": "Rejestracja zakoÅ„czona", "Registration_via_Admin": "Rejestracja przez administratora", "Regular_Expressions": "Wyrażenia regularne", + "Reject_call": "Odrzucenie poÅ‚Ä…czenia", "Release": "Wydanie", + "Releases": "Wydania", "Religious": "Religijny", "Reload": "PrzeÅ‚adować", "Reload_page": "PrzeÅ‚aduj stronÄ™", @@ -3651,6 +3842,8 @@ "Remove_as_leader": "UsuÅ„ jako lidera", "Remove_as_moderator": "UsuÅ„ jako moderator", "Remove_as_owner": "UsuÅ„ jako wÅ‚aÅ›ciciel", + "remove-canned-responses": "UsuÅ„ predefiniowane odpowiedzi", + "remove-canned-responses_description": "Zezwolenie na usuniÄ™cie predefiniowanych odpowiedzi", "Remove_Channel_Links": "UsuÅ„ Å‚Ä…cza do kanałów", "Remove_custom_oauth": "UsuÅ„ wÅ‚asne OAuth", "Remove_from_room": "UsuÅ„ z pokoju", @@ -3658,10 +3851,15 @@ "Remove_last_admin": "Usuwanie ostatniego admina", "Remove_someone_from_room": "UsuÅ„ kogoÅ› z pokoju", "remove-closed-livechat-room": "UsuÅ„ zamkniÄ™ty pokój Omnichannel", + "remove-closed-livechat-room_description": "Pozwolenie na usuniÄ™cie zamkniÄ™tego kanaÅ‚u omnichannel", "remove-closed-livechat-rooms": "UsuÅ„ zamkniÄ™te Livechat Rooms", "remove-closed-livechat-rooms_description": "Zezwolenie na usuniÄ™cie zamkniÄ™tych pokoi omnichannel", "remove-livechat-department": "UsuÅ„ departamenty Omnichannel", + "remove-livechat-department_description": "Zezwolenie na usuniÄ™cie działów omnichannel", "remove-slackbridge-links": "UsuÅ„ linki slackbridge", + "remove-slackbridge-links_description": "Pozwolenie na usuniÄ™cie linków Slackbridge", + "remove-team-channel": "Usunąć zespół Channel", + "remove-team-channel_description": "Zezwolenie na usuniÄ™cie kanaÅ‚u zespoÅ‚u", "remove-user": "UsuÅ„ użytkownika", "remove-user_description": "Zezwolenie na usuniÄ™cie użytkownika z pokoju", "Removed": "UsuniÄ™to", @@ -3681,6 +3879,7 @@ "Report": "ZgÅ‚oÅ› wiadomość", "Report_Abuse": "ZgÅ‚oÅ› nadużycie", "Report_exclamation_mark": "Raport!", + "Report_has_been_sent": "Raport zostaÅ‚ wysÅ‚any", "Report_Number": "Numer zgÅ‚oszenia", "Report_this_message_question_mark": "ZgÅ‚oÅ› tÄ… wiadomość?", "Reporting": "Raportowanie", @@ -3715,6 +3914,7 @@ "Restart": "Uruchom ponownie", "Restart_the_server": "Uruchom serwer ponownie", "restart-server": "Zrestartuj serwer", + "restart-server_description": "Uprawnienie do ponownego uruchomienia serwera", "Retail": "Sprzedaż", "Retention_setting_changed_successfully": "Zmieniono ustawienie zasad przechowywania", "RetentionPolicy": "Zasady przechowywania", @@ -3754,8 +3954,10 @@ "Return_to_home": "Powrót do strony domowej", "Return_to_previous_page": "powróc do poprzedniej strony", "Return_to_the_queue": "Powrót do kolejki", + "Review_devices": "Sprawdź, kiedy i skÄ…d urzÄ…dzenia siÄ™ Å‚Ä…czÄ…", "Ringing": "Dzwonienie", "Robot_Instructions_File_Content": "Zawartość pliku Robots.txt", + "Root": "Root", "Default_Referrer_Policy": "DomyÅ›lne Referrer Policy", "Default_Referrer_Policy_Description": "To kontroluje nagłówek 'referrer', który jest wysyÅ‚any podczas żądania osadzonych mediów z innych serwerów. Aby uzyskać wiÄ™cej informacji, zapoznaj siÄ™ z <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy\">tym linkiem z MDN</a>. PamiÄ™taj, że aby to zadziaÅ‚aÅ‚o, wymagane jest peÅ‚ne odÅ›wieżenie strony", "No_Referrer": "Brak Referrer", @@ -3770,6 +3972,7 @@ "Unsafe_Url": "Niebezpieczny adres URL", "Rocket_Chat_Alert": "Alert Rocket.Chat", "Role": "Rola", + "Roles": "Role", "Role_Editing": "Edycja ról", "Role_Mapping": "Mapowanie ról", "Role_removed": "Rola usuniÄ™ta", @@ -3906,6 +4109,12 @@ "Save_to_enable_this_action": "Zapisz, aby wÅ‚Ä…czyć tÄ… akcjÄ™", "Save_To_Webdav": "Zapisz w WebDAV", "Save_Your_Encryption_Password": "Zapisz swoje hasÅ‚o szyfrowania", + "save-all-canned-responses": "Zapisz wszystkie predefiniowane odpowiedzi", + "save-all-canned-responses_description": "Pozwolenie na zapisanie wszystkich predefiniowanych odpowiedzi", + "save-canned-responses": "Zapisz predefiniowane odpowiedzi", + "save-canned-responses_description": "Zezwolenie na zapisywanie predefiniowanych odpowiedzi", + "save-department-canned-responses": "Zapisz predefiniowane odpowiedzi dla departamentu", + "save-department-canned-responses_description": "Zezwolenie na zapisywanie predefiniowanych odpowiedzi dla departamentu", "save-others-livechat-room-info": "Zapisz inne informacje o pokoju Livechat", "save-others-livechat-room-info_description": "Zezwolenie na zapisywanie informacji z innych kanałów na żywo", "Saved": "Zapisano", @@ -3916,8 +4125,10 @@ "Score": "Wynik", "Screen_Lock": "Blokada ekranu", "Screen_Share": "Współdzielenie ekranu", + "Script": "Skrypt", "Script_Enabled": "Skrypt wÅ‚Ä…czony", "Search": "Szukaj", + "Searchable": "Możliwość wyszukiwania", "Search_Apps": "Szukaj Aplikacji", "Search_by_file_name": "Wyszukaj wedÅ‚ug nazwy pliku", "Search_by_username": "Szukaj wedÅ‚ug nazwy użytkownika", @@ -3926,6 +4137,7 @@ "Search_Chat_History": "Przeszukaj historiÄ™ czatu", "Search_current_provider_not_active": "Obecny dostawca wyszukiwania nie jest aktywny", "Search_Description": "Wybierz dostawcÄ™ wyszukiwania w obszarze roboczym i skonfiguruj ustawienia zwiÄ…zane z wyszukiwaniem.", + "Search_Devices_Users": "Wyszukiwanie urzÄ…dzeÅ„ lub użytkowników", "Search_Files": "Wyszukaj pliki", "Search_for_a_more_general_term": "Szukaj bardziej ogólnego terminu", "Search_for_a_more_specific_term": "Szukaj bardziej szczegółowego terminu", @@ -3943,11 +4155,15 @@ "seconds": "sekund", "Secret_token": "Sekret", "Security": "BezpieczeÅ„stwo", + "See_documentation": "Zobacz w dokumentacji", + "See_Pricing": "Zobacz wycenÄ™", "See_full_profile": "Zobacz peÅ‚ny profil", + "See_history": "Zobacz historiÄ™", "See_on_Engagement_Dashboard": "Zobacz na Engagement Dashboard", "Select_a_department": "Wybierz dziaÅ‚", "Select_a_room": "Wybierz pokój", "Select_a_user": "Wybierz użytkownika", + "Select_a_webdav_server": "Wybierz serwer WebDAV", "Select_an_avatar": "Wybierz awatar", "Select_an_option": "Wybierz opcjÄ™", "Select_at_least_one_user": "Wybierz co najmniej jednego użytkownika", @@ -3997,6 +4213,7 @@ "Send_welcome_email": "WyÅ›lij powitalnego e-maila", "Send_your_JSON_payloads_to_this_URL": "WyÅ›lij swoje dane JSON na ten adresu URL.", "send-mail": "WyÅ›lij wiadomoÅ›ci email.", + "send-mail_description": "Pozwolenie na wysyÅ‚anie e-maili", "send-many-messages": "WyÅ›lij wiele wiadomoÅ›ci", "send-many-messages_description": "Zezwolenie na ominiÄ™cie limitu 5 wiadomoÅ›ci na sekundÄ™", "send-omnichannel-chat-transcript": "WyÅ›lij transkrypt konwersacjo omnichannel", @@ -4035,6 +4252,7 @@ "Setup_Wizard": "Kreator konfiguracji", "Setup_Wizard_Description": "Podstawowe informacje o Twoim obszarze roboczym, takie jak nazwa organizacji i kraj.", "Setup_Wizard_Info": "Poprowadzimy CiÄ™ przez konfigurowanie pierwszego administratora, konfigurowanie organizacji i rejestrowanie serwera, aby otrzymywać bezpÅ‚atne powiadomienia push i wiele wiÄ™cej.", + "Share": "Podziel siÄ™", "Share_Location_Title": "UdostÄ™pnić lokalizacjÄ™?", "Share_screen": "Współdzielenie ekranu", "New_CannedResponse": "Nowa predefiniowana odpowiedź", @@ -4066,6 +4284,7 @@ "Show_Setup_Wizard": "Pokaż Kreatora instalacji", "Show_the_keyboard_shortcut_list": "Pokaż listÄ™ skrótów klawiaturowych", "Show_video": "Pokaż wideo", + "Showing": "Pokazywanie", "Showing_archived_results": "<p>Pokazuje <b>%s</b> zarchiwizowanych wyników </p>", "Showing_online_users": "Pokazywanie: <b>__total_showing__</b>, Online: __online__, Ogółem: __total__ users", "Showing_results": "<p>WyÅ›wietlono <b>%s</b> wyników</p>", @@ -4134,6 +4353,9 @@ "snippet-message_description": "Zezwolenie na tworzenie wiadomoÅ›ci typu snippet", "Snippeted_a_message": "Utworzono fragment __snippetLink__", "Social_Network": "Sieć spoÅ‚eczna", + "Some_ideas_to_get_you_started": "Kilka pomysłów na poczÄ…tek", + "Something_went_wrong": "CoÅ› poszÅ‚o nie tak", + "Something_went_wrong_try_again_later": "CoÅ› poszÅ‚o nie tak, spróbuj ponownie później.", "Sorry_page_you_requested_does_not_exist_or_was_deleted": "Przepraszamy, żądana strona nie istnieje lub zostaÅ‚a usuniÄ™ta!", "Sort": "Sortowanie", "Sort_By": "Sortuj po", @@ -4142,13 +4364,20 @@ "Sound_File_mp3": "Plik dźwiÄ™kowy (mp3)", "Sound File": "Plik dźwiÄ™kowy", "Source": "ŹródÅ‚o", + "Speakers": "GÅ‚oÅ›niki", + "spy-voip-calls": "Szpieguj rozmowy Voip", + "spy-voip-calls_description": "Pozwolenie na szpiegowanie poÅ‚Ä…czeÅ„ voip", "SSL": "SSL", "Star": "Oznacz gwiazdkÄ…", "Star_Message": "Oznacz wiadomość", "Starred_Messages": "Ulubione wiadomoÅ›ci", "Start": "PoczÄ…tek", + "Start_a_call": "Rozpocznij poÅ‚Ä…czenie", + "Start_a_call_with": "Rozpocznij poÅ‚Ä…czenie z", "Start_audio_call": "Rozpocznij rozmowÄ™ audio", + "Start_call": "Rozpocząć poÅ‚Ä…czenie", "Start_Chat": "Rozpocznij czat", + "Start_conference_call": "RozpoczÄ™cie poÅ‚Ä…czenia konferencyjnego", "Start_of_conversation": "Rozpocznij rozmowÄ™", "Start_OTR": "Rozpocznij OTR", "Start_video_call": "Rozpocznij poÅ‚Ä…czenie wideo", @@ -4214,6 +4443,7 @@ "Style": "Styl", "Subject": "Temat", "Submit": "PrzeÅ›lij", + "Subscribe": "Subskrybuj", "Success": "Sukces", "Success_message": "sukces wiadomość", "Successfully_downloaded_file_from_external_URL_should_start_preparing_soon": "PomyÅ›lnie pobrano plik z zewnÄ™trznego adresu URL, powinien wkrótce siÄ™ przygotować", @@ -4230,12 +4460,14 @@ "Sync_success": "Synchronizacja pomyÅ›lna", "Sync_Users": "Użytkownicy Sync", "sync-auth-services-users": "Synchronizuj użytkowników usÅ‚ug uwierzytelniania", + "sync-auth-services-users_description": "Uprawnienie do synchronizacji użytkowników usÅ‚ug uwierzytelniania", "System_messages": "WiadomoÅ›ci systemowe", "Tag": "Tag", "Tags": "Tagi", "Tag_removed": "Tag usuniÄ™ty", "Tag_already_exists": "Tag już istnieje", "Take_it": "Odbierz!", + "Take_rocket_chat_with_you_with_mobile_applications": "Zabierz ze sobÄ… Rocket.Chat dziÄ™ki aplikacjom mobilnym.", "Taken_at": "WziÄ™te w", "Talk_Time": "Czas rozmowy", "Target user not allowed to receive messages": "Docelowy użytkownik nie może odbierać wiadomoÅ›ci", @@ -4306,10 +4538,12 @@ "Teams_New_Read_only_Label": "Tylko do odczytu", "Technology_Services": "UsÅ‚ugi technologiczne", "Terms": "Warunki", + "Terms_of_use": "Warunki użytkowania", "Test_Connection": "Test poÅ‚Ä…czenia", "Test_Desktop_Notifications": "Testuj powiadomienia na pulpicie", "Test_LDAP_Search": "Testuj wyszukiwanie LDAP", "test-admin-options": "Testuj opcje w panelu administracyjnym, takie jak logowanie LDAP i powiadomienia push", + "test-admin-options_description": "Uprawnienie do testowania opcji w panelu administracyjnym takich jak logowanie LDAP i powiadomienia push", "Texts": "Trksty", "Thank_you_for_your_feedback": "DziÄ™kujemy za TwojÄ… opiniÄ™", "The_application_name_is_required": "Wymagana jest nazwa aplikacji", @@ -4398,7 +4632,9 @@ "There_are_no_monitors_added_to_this_unit_yet": "Nie ma monitorów dodanych do tej jednostki", "There_are_no_personal_access_tokens_created_yet": "Nie utworzono jeszcze żadnych tokenów dostÄ™pu osobistego.", "There_are_no_users_in_this_role": "Ta rola nie ma przypisanych użytkowników.", + "There_is_no_video_conference_history_in_this_room": "W tym pokoju nie ma historii poÅ‚Ä…czeÅ„ konferencyjnych", "There_is_one_or_more_apps_in_an_invalid_state_Click_here_to_review": "Jedna lub wiÄ™cej aplikacji ma niepoprawny stan. Kliknij tutaj żeby to sprawdzić.", + "There_has_been_an_error_installing_the_app": "WystÄ…piÅ‚ bÅ‚Ä…d podczas instalacji aplikacji", "These_notes_will_be_available_in_the_call_summary": "Te uwagi bÄ™dÄ… dostÄ™pne w podsumowaniu poÅ‚Ä…czenia", "This_agent_was_already_selected": "Ten agent zostaÅ‚ już wybrany", "this_app_is_included_with_subscription": "Ta aplikacja jest doÅ‚Ä…czona do subskrypcji __bundleName__", @@ -4418,6 +4654,7 @@ "Thread_message": "Skomentowane *__username__'s* message: _ __msg__ _", "Threads": "WÄ…tki", "Threads_Description": "WÄ…tki umożliwiajÄ… zorganizowane dyskusje wokół okreÅ›lonej wiadomoÅ›ci.", + "Threads_unavailable_for_federation": "WÄ…tki sÄ… niedostÄ™pne dla sfederowanych kanałów", "Thursday": "Czwartek", "Time_in_minutes": "Czas w minutach", "Time_in_seconds": "Czas w sekundach", @@ -4431,6 +4668,7 @@ "To": "Do", "To_additional_emails": "Do dodatkowych wiadomoÅ›ci e-mail", "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site": "Aby zainstalować Rocket.Chat LiveChat na swojej stronie, skopiuj i wklej powyższy kod nad ostatni <strong></body></strong> na swojej stronie.", + "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "Aby uniknąć ponownego wyÅ›wietlania tego komunikatu, upewnij siÄ™, że ustawienia przeglÄ…darki zezwalajÄ… na otwieranie wyskakujÄ…cych okienek z adresu URL obszaru roboczego ", "to_see_more_details_on_how_to_integrate": "aby zobaczyć wiÄ™cej szczegółów na temat integracji.", "To_users": "Do użytkowników", "Today": "Dzisiaj", @@ -4440,6 +4678,7 @@ "Token": "Token", "Token_Access": "DostÄ™p tokenowy", "Token_Controlled_Access": "DostÄ™p kontrolowany tokenem", + "Token_has_been_removed": "Token zostaÅ‚ usuniÄ™ty", "Token_required": "Wymagany token", "Tokens_Minimum_Needed_Balance": "Minimalne wymagane saldo tokenów", "Tokens_Minimum_Needed_Balance_Description": "Ustaw minimalne wymagane saldo na każdym tokenie. Puste lub \"0\" bez ograniczenia.", @@ -4539,6 +4778,7 @@ "unarchive-room_description": "Zezwolenie na cofniÄ™cie archiwizacji kanałów", "Unassigned": "Nieprzypisane", "Unavailable": "NiedostÄ™pne", + "Unblock": "Odblokuj", "Unblock_User": "Odblokuj użytkownika", "Uncheck_All": "Odznacz wszystkie", "Uncollapse": "Uncollapse", @@ -4592,6 +4832,7 @@ "Uploading_file": "PrzesyÅ‚anie pliku ...", "Uptime": "Czas pracy", "URL": "URL", + "URLs": "Adresy URL", "Usage": "Użycie", "Use": "Użyj", "Use_account_preference": "Użyj preferencji konta", @@ -4741,30 +4982,73 @@ "Verify_your_email_for_the_code_we_sent": "Zweryfikuj swój e-mail pod kÄ…tem wysÅ‚anego przez nas kodu", "Version": "Wersja", "Version_version": "Wersja __version__", + "Video_Conference_Description": "Skonfiguruj poÅ‚Ä…czenia konferencyjne dla swojej przestrzeni roboczej.", "Video_Chat_Window": "Czat Video", "Video_Conference": "Konferencja wideo", + "Video_Call_unavailable_for_federation": "PoÅ‚Ä…czenie wideo jest niedostÄ™pne dla kanałów skonfederowanych", + "Video_Conferences": "Telekonferencje", + "video-conf-provider-not-configured": "**PoÅ‚Ä…czenie konferencyjne nie jest wÅ‚Ä…czone**: Administrator obszaru roboczego musi najpierw wÅ‚Ä…czyć funkcjÄ™ poÅ‚Ä…czeÅ„ konferencyjnych.", + "Video_conference_app_required": "Wymagana aplikacja do poÅ‚Ä…czeÅ„ konferencyjnych", + "Video_conference_apps_available": "Aplikacje do poÅ‚Ä…czeÅ„ konferencyjnych sÄ… dostÄ™pne w marketplace Rocket.Chat.", + "Video_conference_apps_can_be_installed": "Aplikacje do poÅ‚Ä…czeÅ„ konferencyjnych mogÄ… być instalowane w marketplace Rocket.Chat przez administratora przestrzeni roboczej.", + "Video_conference_not_available": "PoÅ‚Ä…czenie konferencyjne nie jest dostÄ™pne", "Video_message": "Wiadomość wideo", "Videocall_declined": "Rozmowa video odrzucona.", "Video_and_Audio_Call": "PoÅ‚Ä…czenie Audio Wideo", + "video_conference_started": "_Zaczęło siÄ™ poÅ‚Ä…czenie._", + "video_conference_started_by": "**__username__** _rozpoczÄ…Å‚ poÅ‚Ä…czenie._", + "video_conference_ended": "_poÅ‚Ä…czenie zakoÅ„czone_.", + "video_conference_ended_by": "**__username__** _zakoÅ„czyÅ‚ poÅ‚Ä…czenie._", + "video_livechat_started": "_Zaczęła siÄ™ rozmowa wideo._", + "video_livechat_missed": "_rozpoczęła siÄ™ rozmowa wideo, która nie zostaÅ‚a odebrana._", "video_direct_calling": "dzwoni.", + "video_direct_ended": "_poÅ‚Ä…czenie zakoÅ„czone_", + "video_direct_ended_by": "**__username__** _zakoÅ„czyÅ‚ rozmowÄ™._", + "video_direct_missed": "_rozpoczÄ…Å‚ poÅ‚Ä…czenie, które nie zostaÅ‚o odebrane_", + "video_direct_started": "_Zaczęło siÄ™ poÅ‚Ä…czenie._", + "VideoConf_Default_Provider": "Dostawca domyÅ›lny", + "VideoConf_Default_Provider_Description": "JeÅ›li masz zainstalowanych kilka aplikacji dostawcy, wybierz, która z nich ma być używana do nowych poÅ‚Ä…czeÅ„ konferencyjnych.", + "VideoConf_Enable_Channels": "WÅ‚Ä…cz w kanaÅ‚ach publicznych", + "VideoConf_Enable_Groups": "WÅ‚Ä…cz w kanaÅ‚ach prywatnych", + "VideoConf_Enable_DMs": "WÅ‚Ä…cz w wiadomoÅ›ciach bezpoÅ›rednich", + "VideoConf_Enable_Teams": "WÅ‚Ä…cz w zespoÅ‚ach", + "videoconf-ring-users": "Informuj pozostaÅ‚ych użytkowników gdy dzwonisz", + "videoconf-ring-users_description": "Zezwolenie na informowanie pozostaÅ‚ych użytkowników gdy dzwonisz", "Videos": "Wideo", "View_All": "Pokaż wszystko", "View_channels": "WyÅ›wietl kanaÅ‚y", + "view-agent-canned-responses": "WyÅ›wietlanie predefiniowanych odpowiedzi agenta", + "view-agent-canned-responses_description": "Uprawnienie do wyÅ›wietlania predefiniowanych odpowiedzi agenta", + "view-agent-extension-association": "Zobacz asocjacje rozszerzeÅ„ agenta", + "view-agent-extension-association_description": "Zezwolenie na przeglÄ…danie asocjacji rozszerzeÅ„ agenta", + "view-all-canned-responses": "WyÅ›wietl wszystkie predefiniowane odpowiedzi", + "view-all-canned-responses_description": "Uprawnienie do przeglÄ…dania wszystkich predefiniowanych odpowiedzi", "view-import-operations": "WyÅ›wietl operacje importu", + "view-import-operations_description": "Uprawnienia do przeglÄ…dania operacji importu", "view-omnichannel-contact-center": "Zobacz Omnichannel Contact Center", "view-omnichannel-contact-center_description": "Uprawnienia do przeglÄ…dania i interakcji z Omnichannel Contact Center", "View_Logs": "Zobacz logi", "View_mode": "Tryb podglÄ…du", "View_original": "WyÅ›wietl oryginaÅ‚", "View_the_Logs_for": "WyÅ›wietl dzienniki dla: „__name__â€", + "view-all-teams": "WyÅ›wietl wszystkie zespoÅ‚y", + "view-all-teams_description": "Uprawnienie do przeglÄ…dania wszystkich zespołów", + "view-all-team-channels": "WyÅ›wietl wszystkie zespoÅ‚y Channel", + "view-all-team-channels_description": "Uprawnienie do przeglÄ…dania wszystkich kanałów zespoÅ‚u", "view-broadcast-member-list": "WyÅ›wietl listÄ™ czÅ‚onków w pokoju rozgÅ‚oszeniowym", "view-broadcast-member-list_description": "Uprawnienie do przeglÄ…dania listy użytkowników w kanale broadcastowym", "view-c-room": "Zobacz kanaÅ‚ publiczny", "view-c-room_description": "Zezwolenie na oglÄ…danie kanałów publicznych", "view-canned-responses": "Pokaż predefiniowane odpowiedzi", + "view-canned-responses_description": "Zezwolenie na wyÅ›wietlanie predefiniowanych odpowiedzi", "view-d-room": "Zobacz bezpoÅ›rednie wiadomoÅ›ci", "view-d-room_description": "Uprawnienie do przeglÄ…dania bezpoÅ›rednich wiadomoÅ›ci", + "view-device-management": "WyÅ›wietlanie zarzÄ…dzania urzÄ…dzeniami", + "view-device-management_description": "Uprawnienie do przeglÄ…dania pulpitu nawigacyjnego zarzÄ…dzania urzÄ…dzeniami", + "view-engagement-dashboard": "Zobacz tablicÄ™ wskaźników zaangażowania", + "view-engagement-dashboard_description": "Pozwolenie na przeglÄ…danie tablicy wskaźników zaangażowania", "view-federation-data": "WyÅ›wietl dane federacji", + "view-federation-data_description": "Uprawnienie do przeglÄ…dania danych federacji", "View_full_conversation": "Zobacz peÅ‚nÄ… rozmowÄ™", "view-full-other-user-info": "WyÅ›wietl peÅ‚nÄ… informacjÄ™ o użytkowniku", "view-full-other-user-info_description": "Zezwolenie na wyÅ›wietlanie peÅ‚nego profilu innych użytkowników, w tym daty utworzenia konta, ostatniego logowania itp.", @@ -4784,12 +5068,22 @@ "view-livechat-business-hours_description": "Uprawnienie do wyÅ›wietlania godzin pracy livechat", "view-livechat-current-chats": "Zobacz aktualne czaty Omnichannel", "view-livechat-current-chats_description": "Uprawnienie do przeglÄ…dania bieżących czatów livechat", + "view-livechat-customfields": "WyÅ›wietlanie pól wÅ‚asnych Omnichannel", + "view-livechat-customfields_description": "Uprawnienia do przeglÄ…dania pól wÅ‚asnych Omnichannel", "view-livechat-departments": "Zobacz dziaÅ‚y Livechat", + "view-livechat-departments_description": "Uprawnienia do przeglÄ…dania działów Omnichannel", + "view-livechat-facebook": "Zobacz Omnichannel Facebooku", + "view-livechat-facebook_description": "Pozwolenie na oglÄ…danie Omnichannel Facebook", + "view-livechat-installation": "Zobacz instalacjÄ™ omnichannel", + "view-livechat-installation_description": "Uprawnienie do przeglÄ…dania instalacji Omnichannel", "view-livechat-manager": "WyÅ›wietl Menedżera Livechat", "view-livechat-manager_description": "Zezwolenie na przeglÄ…danie innych menedżerów na żywo", "view-livechat-monitor": "Zobacz monitory Livechat", "view-livechat-queue": "Zobacz kolejkÄ™ Livechat", + "view-livechat-queue_description": "Uprawnienie do przeglÄ…dania kolejki Omnichannel", + "view-livechat-real-time-monitoring": "Zobacz monitoring w czasie rzeczywistym Omnichannel ", "view-livechat-room-closed-by-another-agent": "WyÅ›wietl Omnichannel Rooms zamkniÄ™ty przez innego agenta", + "view-livechat-room-closed-by-another-agent_description": "Uprawnienie do wyÅ›wietlania pokoi live chat zamkniÄ™tych przez innego agenta", "view-livechat-room-closed-same-department": "Zobacz Omnichannel Rooms zamkniÄ™ty przez innego agenta w tym samym dziale", "view-livechat-room-closed-same-department_description": "Uprawnienie do przeglÄ…dania pokojów livechat zamkniÄ™tych przez innego agenta z tego samego departamentu", "view-livechat-room-customfields": "WyÅ›wietl pola niestandardowe dla kanaÅ‚u omnichannel", @@ -4860,6 +5154,7 @@ "VoIP_JWT_Secret_description": "Pozwala na ustawienie tajnego klucza do udostÄ™pniania klientowi szczegółów rozszerzenia z serwera jako JWT zamiast zwykÅ‚ego tekstu. JeÅ›li opcja ta nie zostanie skonfigurowana, szczegóły rejestracji rozszerzenia bÄ™dÄ… wysyÅ‚ane jako zwykÅ‚y tekst", "Voip_is_disabled": "VoIP jest wyÅ‚Ä…czony", "Voip_is_disabled_description": "Aby wyÅ›wietlić listÄ™ rozszerzeÅ„, należy aktywować VoIP, zrób to w zakÅ‚adce Ustawienia.", + "VoIP_Toggle": "WÅ‚Ä…czanie/wyÅ‚Ä…czanie VoIP", "Chat_opened_by_visitor": "Czat otwarty przez goÅ›cia", "Wait_activation_warning": "Zanim siÄ™ zalogujesz, twoje konto musi być aktywowane przez administratora.", "Waiting_queue": "Kolejka oczekujÄ…cych", @@ -4871,6 +5166,7 @@ "WAU_value": "WAU __value__", "We_appreciate_your_feedback": "Doceniamy TwojÄ… opiniÄ™", "We_are_offline_Sorry_for_the_inconvenience": "JesteÅ›my offline. Przepraszamy za niedogodnoÅ›ci.", + "We_Could_not_retrive_any_data": "Nie mogliÅ›my odzyskać żadnych danych", "We_have_sent_password_email": "WysÅ‚aliÅ›my Ci e-mail z instrukcjami resetowania hasÅ‚a. JeÅ›li nie dostaniesz wiadomoÅ›ci, prosimy spróbować ponownie.", "We_have_sent_registration_email": "WysÅ‚aliÅ›my e-mail w celu potwierdzenie Twojej rejestracji. JeÅ›li nie dostaniesz wiadomoÅ›ci, prosimy spróbować ponownie.", "Webdav Integration": "Integracja Webdav", @@ -4890,6 +5186,7 @@ "WebRTC": "WebRTC", "WebRTC_Description": "Emituj materiaÅ‚y audio i/lub wideo, a także przesyÅ‚aj dowolne dane miÄ™dzy przeglÄ…darkami bez poÅ›rednictwa.", "WebRTC_Call": "PoÅ‚Ä…czenie WebRTC", + "WebRTC_Call_unavailable_for_federation": "PoÅ‚Ä…czenie WebRTC jest niedostÄ™pne dla pokoi sfederowanych", "WebRTC_direct_audio_call_from_%s": "BezpoÅ›rednie poÅ‚Ä…czenie audio z%s", "WebRTC_direct_video_call_from_%s": "BezpoÅ›rednie poÅ‚Ä…czenie wideo z%s", "WebRTC_Enable_Channel": "WÅ‚Ä…cz dla kanałów publicznych", @@ -4918,6 +5215,7 @@ "will_be_able_to": "bÄ™dzie zdolny do", "Will_be_available_here_after_saving": "BÄ™dzie. dostÄ™pne tutaj po zapisaniu.", "Without_priority": "Bez priorytetu", + "Workspace_now_using_device_management": "PrzestrzeÅ„ robocza wykorzystuje teraz zarzÄ…dzanie urzÄ…dzeniami", "Worldwide": "Na calym swiecie", "Would_you_like_to_return_the_inquiry": "Czy chcesz zwrócić zapytanie?", "Would_you_like_to_return_the_queue": "Czy chcesz przenieść ten pokój z powrotem do kolejki? CaÅ‚a historia rozmów zostanie zachowana w pokoju.", @@ -4938,6 +5236,7 @@ "yesterday": "wczoraj", "Yesterday": "Wczoraj", "You": "ty", + "You_reacted_with": "ZareagowaÅ‚eÅ› z __emoji__", "Users_reacted_with": "__users__ zareagowali z __emoji__", "Users_and_more_reacted_with": null, "You_and_users_Reacted_with": "Ty i __users__ zareagowali z __emoji__", @@ -4999,6 +5298,7 @@ "Your_server_link": "Twój link do serwera", "Your_temporary_password_is_password": "Twoje tymczasowe hasÅ‚o to: <strong>[password]</strong>", "Your_TOTP_has_been_reset": "Twoje dwuskÅ‚adnikowe TOTP zostaÅ‚o zresetowane", + "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "Twoja przeglÄ…darka internetowa zablokowaÅ‚a Rocket.Chat przed otwarciem nowej karty.", "Your_workspace_is_ready": "Twój obszar roboczy jest gotowy do użycia 🎉", "Zapier": "Zapier", "onboarding.component.form.steps": "Krok {{currentStep}} z {{stepCount}}", @@ -5075,5 +5375,18 @@ "onboarding.form.standaloneServerForm.title": "Potwierdzenie serwera standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usÅ‚ug bÄ™dÄ… niedostÄ™pne lub bÄ™dÄ… wymagaÅ‚y rÄ™cznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyÅ‚ania powiadomieÅ„ push należy skompilować i opublikować wÅ‚asnÄ… aplikacjÄ™ w Google Play i App Store", - "onboarding.form.standaloneServerForm.manuallyIntegrate": "Konieczność rÄ™cznej integracji z usÅ‚ugami zewnÄ™trznymi" + "onboarding.form.standaloneServerForm.manuallyIntegrate": "Konieczność rÄ™cznej integracji z usÅ‚ugami zewnÄ™trznymi", + "Login_Detected": "Wykryto logowanie", + "Logged_In_Via": "Zalogowano siÄ™ za poÅ›rednictwem", + "Access_Your_Account": "DostÄ™p do konta", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Możesz też skopiować i wkleić ten adres w zakÅ‚adce swojej przeglÄ…darki", + "Thank_You_For_Choosing_RocketChat": "DziÄ™kujemy za wybranie Rocket.Chat!", + "Device_Management_Client": "Klient", + "Device_Management_OS": "OS", + "Device_Management_Device": "UrzÄ…dzenie", + "Device_Management_Device_Unknown": "Nieznane", + "Device_Management_IP": "IP", + "Device_Management_Email_Subject": "[Site_Name] - Login wykryty", + "Device_Management_Email_Body": "Możesz użyć nastÄ™pujÄ…cych placeholderów: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", + "Something_Went_Wrong": "CoÅ› poszÅ‚o nie tak" } \ No newline at end of file -- GitLab From 01c878625a8705f7555f9d069eb057e9a1b70a26 Mon Sep 17 00:00:00 2001 From: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:09:40 -0300 Subject: [PATCH 012/107] [FIX] Menu options margin spacing (#26775) --- .../client/sidebar/header/UserDropdown.tsx | 2 +- apps/meteor/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- yarn.lock | 90 +++++++++++++++---- 7 files changed, 80 insertions(+), 22 deletions(-) diff --git a/apps/meteor/client/sidebar/header/UserDropdown.tsx b/apps/meteor/client/sidebar/header/UserDropdown.tsx index b2485124898..c96bf515bdc 100644 --- a/apps/meteor/client/sidebar/header/UserDropdown.tsx +++ b/apps/meteor/client/sidebar/header/UserDropdown.tsx @@ -109,7 +109,7 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { return ( <Box display='flex' flexDirection='column' w={!isMobile ? '244px' : undefined}> - <Box pi='x16' display='flex' flexDirection='row' alignItems='center'> + <Box pi='x12' display='flex' flexDirection='row' alignItems='center'> <Box mie='x4'> <UserAvatar size='x36' username={username || ''} etag={avatarETag} /> </Box> diff --git a/apps/meteor/package.json b/apps/meteor/package.json index e843d12476b..c4b9da17759 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -206,7 +206,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "0.31.18", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "0.31.18", "@rocket.chat/fuselage-polyfills": "0.31.18", "@rocket.chat/fuselage-toastbar": "0.31.18", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index b8aac2531a8..faf9c4db011 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -49,7 +49,7 @@ "devDependencies": { "@rocket.chat/apps-engine": "~1.30.0", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "0.31.18", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "0.31.18", "@rocket.chat/fuselage-polyfills": "0.31.18", "@rocket.chat/icons": "0.31.18", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b3f4ea5d3e8..c5afba1ef46 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -7,7 +7,7 @@ "@mdx-js/react": "^1.6.22", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "0.31.18", - "@rocket.chat/fuselage": "0.31.18", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-tokens": "0.31.18", "@rocket.chat/message-parser": "0.31.18", "@rocket.chat/styled": "0.31.18", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index f8ee71e8f01..bd9d270b3d8 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "0.31.18", - "@rocket.chat/fuselage": "0.31.18", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "0.31.18", "@rocket.chat/icons": "0.31.18", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7f04a530b76..e96b6c1f048 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@rocket.chat/css-in-js": "0.31.18", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "0.31.18", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "0.31.18", "@rocket.chat/styled": "0.31.18", "@types/jest": "^27.4.1", diff --git a/yarn.lock b/yarn.lock index ff3525b6d9d..b2d57ae5aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3333,6 +3333,19 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-in-js@npm:~0.31.19-dev.10": + version: 0.31.19-dev.10 + resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.10" + dependencies: + "@emotion/hash": ^0.8.0 + "@rocket.chat/css-supports": ~0.31.19-dev.10 + "@rocket.chat/memo": ~0.31.19-dev.10 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.10 + stylis: ~4.0.13 + checksum: 9e69aae1bb1243abbc4a9fd864817e38d8ace88e80f39f9c58701afcf589cb8080d308943c431bea8ff48854dec756637bc8bb4d0e5cdad621406050cb86fd46 + languageName: node + linkType: hard + "@rocket.chat/css-supports@npm:^0.31.18": version: 0.31.18 resolution: "@rocket.chat/css-supports@npm:0.31.18" @@ -3342,6 +3355,15 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-supports@npm:~0.31.19-dev.10": + version: 0.31.19-dev.10 + resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.10" + dependencies: + "@rocket.chat/memo": ~0.31.19-dev.10 + checksum: 4374ebc9bf43d3b6ab42d1f2daa5d426950d32a49de19b97e1f54214e1b608d21b9311883419138774bb41d220e5e23ab5195d1b1ae40b6d03d931502c488b15 + languageName: node + linkType: hard + "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer": version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" @@ -3527,13 +3549,20 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:0.31.18, @rocket.chat/fuselage-tokens@npm:^0.31.18": +"@rocket.chat/fuselage-tokens@npm:0.31.18": version: 0.31.18 resolution: "@rocket.chat/fuselage-tokens@npm:0.31.18" checksum: b8dc4c9ca4abef4c4d95c5b927b2f5a6a4aa38cee4cb22c58425f7423edf5424966de161af2686904be6c6fb4da0f3174b13e10872ee3a05cc6367aaadf0153a languageName: node linkType: hard +"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.92": + version: 0.32.0-dev.92 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.92" + checksum: 57121863319fed1a69f05f95eb28478bd8e8a3fd0b99712adf61a22f0f88b874f65242b53bb5353c06a949def0e0f737fdba377f1053ff6662db9887fd9658cd + languageName: node + linkType: hard + "@rocket.chat/fuselage-ui-kit@npm:0.31.18": version: 0.31.18 resolution: "@rocket.chat/fuselage-ui-kit@npm:0.31.18" @@ -3558,7 +3587,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ~1.30.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": 0.31.18 + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": 0.31.18 "@rocket.chat/fuselage-polyfills": 0.31.18 "@rocket.chat/icons": 0.31.18 @@ -3598,15 +3627,15 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage@npm:0.31.18" +"@rocket.chat/fuselage@npm:next": + version: 0.32.0-dev.142 + resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.142" dependencies: - "@rocket.chat/css-in-js": ^0.31.18 - "@rocket.chat/css-supports": ^0.31.18 - "@rocket.chat/fuselage-tokens": ^0.31.18 - "@rocket.chat/memo": ^0.31.18 - "@rocket.chat/styled": ^0.31.18 + "@rocket.chat/css-in-js": ~0.31.19-dev.10 + "@rocket.chat/css-supports": ~0.31.19-dev.10 + "@rocket.chat/fuselage-tokens": ~0.32.0-dev.92 + "@rocket.chat/memo": ~0.31.19-dev.10 + "@rocket.chat/styled": ~0.31.19-dev.10 invariant: ^2.2.4 react-keyed-flatten-children: ^1.3.0 peerDependencies: @@ -3616,7 +3645,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: bc78ea2d302381ab122ceccbc77b3a97653aa8d9293b25009e6f37b04d59ff608371c9dce2de243651199c6bb9e6d176c6f19d1022bad9a71dfd705caca9590f + checksum: fd9f30f2229c8e41b46e23b37beb5acc58c796a06cfc0fd1f7c1c877b349db257af58b31a12ab4a5606b6a6d8ae268ad2424893226d93cb373bcb8207bfbb656 languageName: node linkType: hard @@ -3628,7 +3657,7 @@ __metadata: "@mdx-js/react": ^1.6.22 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": 0.31.18 - "@rocket.chat/fuselage": 0.31.18 + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-tokens": 0.31.18 "@rocket.chat/message-parser": 0.31.18 "@rocket.chat/styled": 0.31.18 @@ -3804,6 +3833,13 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/memo@npm:~0.31.19-dev.10": + version: 0.31.19-dev.10 + resolution: "@rocket.chat/memo@npm:0.31.19-dev.10" + checksum: de95f407d449c04bd61a4a1c9aea81ba5ee0bb421c168b9297d49de60fcb04b6ae6aa0e3f8ebca03ba38b815238c0fe367541a171943bda0521598ee7c429d64 + languageName: node + linkType: hard + "@rocket.chat/message-parser@npm:0.31.18": version: 0.31.18 resolution: "@rocket.chat/message-parser@npm:0.31.18" @@ -3843,7 +3879,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 - "@rocket.chat/fuselage": 0.31.18 + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": 0.31.18 "@rocket.chat/fuselage-polyfills": 0.31.18 "@rocket.chat/fuselage-toastbar": 0.31.18 @@ -4266,6 +4302,16 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/styled@npm:~0.31.19-dev.10": + version: 0.31.19-dev.10 + resolution: "@rocket.chat/styled@npm:0.31.19-dev.10" + dependencies: + "@rocket.chat/css-in-js": ~0.31.19-dev.10 + tslib: ^2.3.1 + checksum: 0ebfbdbd374e2ef861bae34344160bbdf0a9aafe6a29b455202326c25daaf36e3b934b773938ca03e2e5b95166e5996fb23b93a1ff033034e8f1fd59e2791b89 + languageName: node + linkType: hard + "@rocket.chat/stylis-logical-props-middleware@npm:^0.31.18": version: 0.31.18 resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.18" @@ -4278,12 +4324,24 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.10": + version: 0.31.19-dev.10 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.10" + dependencies: + "@rocket.chat/css-supports": ~0.31.19-dev.10 + tslib: ^2.3.1 + peerDependencies: + stylis: 4.0.10 + checksum: d2a2baac81ae7bd1dfe62dbbc14350eda95c1419f211648f1ff4148d3700a4270cb5c4922dd79b3276257af09efcd42d95c6b898653b2e67fbae1f1b64ff245a + languageName: node + linkType: hard + "@rocket.chat/ui-client@workspace:^, @rocket.chat/ui-client@workspace:packages/ui-client": version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" dependencies: "@rocket.chat/css-in-js": 0.31.18 - "@rocket.chat/fuselage": 0.31.18 + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": 0.31.18 "@rocket.chat/icons": 0.31.18 "@rocket.chat/ui-contexts": "workspace:~" @@ -4373,7 +4431,7 @@ __metadata: dependencies: "@rocket.chat/css-in-js": 0.31.18 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": 0.31.18 + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": 0.31.18 "@rocket.chat/styled": 0.31.18 "@types/jest": ^27.4.1 @@ -20301,7 +20359,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d + checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 languageName: node linkType: hard -- GitLab From 02d5f70e1cd5fa0ad76d257802cc2bc7f3417649 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 6 Sep 2022 15:12:03 -0300 Subject: [PATCH 013/107] [IMPROVE] System messages' consistency (#26130) Co-authored-by: Luciano Marcos Pierdona Junior <64279791+LucianoPierdona@users.noreply.github.com> --- .../server/functions/saveRoomType.js | 4 +- apps/meteor/app/lib/lib/MessageTypes.ts | 102 +++++------------- apps/meteor/app/livechat/lib/messageTypes.ts | 2 +- .../server/cronProcessDownloads.js | 28 ++--- apps/meteor/client/startup/messageTypes.ts | 13 +-- .../MessageList/lib/isOwnUserMessage.spec.ts | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 33 +++++- 7 files changed, 79 insertions(+), 105 deletions(-) diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomType.js b/apps/meteor/app/channel-settings/server/functions/saveRoomType.js index 206520b01b2..cf76ca65193 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomType.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomType.js @@ -41,11 +41,11 @@ export const saveRoomType = function (rid, roomType, user, sendMessage = true) { if (sendMessage) { let message; if (roomType === 'c') { - message = TAPi18n.__('Channel', { + message = TAPi18n.__('public', { lng: (user && user.language) || settings.get('Language') || 'en', }); } else { - message = TAPi18n.__('Private_Group', { + message = TAPi18n.__('private', { lng: (user && user.language) || settings.get('Language') || 'en', }); } diff --git a/apps/meteor/app/lib/lib/MessageTypes.ts b/apps/meteor/app/lib/lib/MessageTypes.ts index ebed349dc54..ef678e46883 100644 --- a/apps/meteor/app/lib/lib/MessageTypes.ts +++ b/apps/meteor/app/lib/lib/MessageTypes.ts @@ -7,29 +7,27 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'r', system: true, - message: 'Room_name_changed', + message: 'Room_name_changed_to', data(message) { return { room_name: message.msg, - user_by: message.u.username, }; }, }); MessageTypes.registerType({ id: 'au', system: true, - message: 'User_added_by', + message: 'User_added_to', data(message) { return { user_added: message.msg, - user_by: message.u.username, }; }, }); MessageTypes.registerType({ id: 'added-user-to-team', system: true, - message: 'Added__username__to_team', + message: 'Added__username__to_this_team', data(message) { return { user_added: message.msg, @@ -39,18 +37,17 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'ru', system: true, - message: 'User_removed_by', + message: 'User_has_been_removed', data(message) { return { user_removed: message.msg, - user_by: message.u.username, }; }, }); MessageTypes.registerType({ id: 'removed-user-from-team', system: true, - message: 'Removed__username__from_team', + message: 'Removed__username__from_the_team', data(message) { return { user_removed: message.msg, @@ -60,27 +57,17 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'ul', system: true, - message: 'User_left', - data(message) { - return { - user_left: message.u.username, - }; - }, + message: 'User_left_this_channel', }); MessageTypes.registerType({ id: 'ult', system: true, - message: 'User_left_team', - data(message) { - return { - user_left: message.u.username, - }; - }, + message: 'User_left_this_team', }); MessageTypes.registerType({ id: 'user-converted-to-team', system: true, - message: 'Converted__roomName__to_team', + message: 'Converted__roomName__to_a_team', data(message) { return { roomName: message.msg, @@ -90,7 +77,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'user-converted-to-channel', system: true, - message: 'Converted__roomName__to_channel', + message: 'Converted__roomName__to_a_channel', data(message) { return { roomName: message.msg, @@ -100,7 +87,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'user-removed-room-from-team', system: true, - message: 'Removed__roomName__from_this_team', + message: 'Removed__roomName__from_the_team', data(message) { return { roomName: message.msg, @@ -110,7 +97,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'user-deleted-room-from-team', system: true, - message: 'Deleted__roomName__', + message: 'Deleted__roomName__room', data(message) { return { roomName: message.msg, @@ -120,7 +107,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'user-added-room-to-team', system: true, - message: 'added__roomName__to_team', + message: 'added__roomName__to_this_team', data(message) { return { roomName: message.msg, @@ -130,7 +117,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'uj', system: true, - message: 'User_joined_channel', + message: 'User_joined_the_channel', data(message) { return { user: message.u.username, @@ -140,7 +127,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'ujt', system: true, - message: 'User_joined_team', + message: 'User_joined_the_team', data(message) { return { user: message.u.username, @@ -150,7 +137,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'ut', system: true, - message: 'User_joined_conversation', + message: 'User_joined_the_conversation', data(message) { return { user: message.u.username, @@ -170,12 +157,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'rm', system: true, - message: 'Message_removed', - data(message) { - return { - user: message.u.username, - }; - }, + message: 'Message_is_removed', }); // MessageTypes.registerType({ // id: 'rtc', @@ -186,22 +168,20 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'user-muted', system: true, - message: 'User_muted_by', + message: 'User_has_been_muted', data(message) { return { user_muted: message.msg, - user_by: message.u.username, }; }, }); MessageTypes.registerType({ id: 'user-unmuted', system: true, - message: 'User_unmuted_by', + message: 'User_has_been_unmuted', data(message) { return { user_unmuted: message.msg, - user_by: message.u.username, }; }, }); @@ -232,67 +212,37 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'room-archived', system: true, - message: 'This_room_has_been_archived_by__username_', - data(message) { - return { - username: message.u.username, - }; - }, + message: 'This_room_has_been_archived', }); MessageTypes.registerType({ id: 'room-unarchived', system: true, - message: 'This_room_has_been_unarchived_by__username_', - data(message) { - return { - username: message.u.username, - }; - }, + message: 'This_room_has_been_unarchived', }); MessageTypes.registerType({ id: 'room-removed-read-only', system: true, - message: 'room_removed_read_only', - data(message) { - return { - user_by: message.u.username, - }; - }, + message: 'room_removed_read_only_permission', }); MessageTypes.registerType({ id: 'room-set-read-only', system: true, - message: 'room_set_read_only', - data(message) { - return { - user_by: message.u.username, - }; - }, + message: 'room_set_read_only_permission', }); MessageTypes.registerType({ id: 'room-allowed-reacting', system: true, - message: 'room_allowed_reacting', - data(message) { - return { - user_by: message.u.username, - }; - }, + message: 'room_allowed_reactions', }); MessageTypes.registerType({ id: 'room-disallowed-reacting', system: true, - message: 'room_disallowed_reacting', - data(message) { - return { - user_by: message.u.username, - }; - }, + message: 'room_disallowed_reactions', }); MessageTypes.registerType({ id: 'room_e2e_enabled', system: true, - message: 'This_room_encryption_has_been_enabled_by__username_', + message: 'Enabled_E2E_Encryption_for_this_room', data(message) { return { username: message.u.username, @@ -302,7 +252,7 @@ Meteor.startup(function () { MessageTypes.registerType({ id: 'room_e2e_disabled', system: true, - message: 'This_room_encryption_has_been_disabled_by__username_', + message: 'Disabled_E2E_Encryption_for_this_room', data(message) { return { username: message.u.username, diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index 1ee3397af6e..65872433a25 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -108,7 +108,7 @@ MessageTypes.registerType({ } return escapeHTML(message.msg); }, - message: 'room_changed_privacy', + message: 'room_changed_type', }); MessageTypes.registerType({ diff --git a/apps/meteor/app/user-data-download/server/cronProcessDownloads.js b/apps/meteor/app/user-data-download/server/cronProcessDownloads.js index 7d657225383..8afbf39f86e 100644 --- a/apps/meteor/app/user-data-download/server/cronProcessDownloads.js +++ b/apps/meteor/app/user-data-download/server/cronProcessDownloads.js @@ -157,67 +157,67 @@ const getMessageData = function (msg, hideUsers, userData, usersMap) { switch (msg.t) { case 'uj': - messageObject.msg = TAPi18n.__('User_joined_channel'); + messageObject.msg = TAPi18n.__('User_joined_the_channel'); break; case 'ul': - messageObject.msg = TAPi18n.__('User_left'); + messageObject.msg = TAPi18n.__('User_left_this_channel'); break; case 'ult': - messageObject.msg = TAPi18n.__('User_left_team'); + messageObject.msg = TAPi18n.__('User_left_this_team'); break; case 'user-added-room-to-team': - messageObject.msg = TAPi18n.__('added__roomName__to_team', { + messageObject.msg = TAPi18n.__('added__roomName__to_this_team', { roomName: msg.msg, }); break; case 'user-converted-to-team': - messageObject.msg = TAPi18n.__('Converted__roomName__to_team', { + messageObject.msg = TAPi18n.__('Converted__roomName__to_a_team', { roomName: msg.msg, }); break; case 'user-converted-to-channel': - messageObject.msg = TAPi18n.__('Converted__roomName__to_channel', { + messageObject.msg = TAPi18n.__('Converted__roomName__to_a_channel', { roomName: msg.msg, }); break; case 'user-deleted-room-from-team': - messageObject.msg = TAPi18n.__('Deleted__roomName__', { + messageObject.msg = TAPi18n.__('Deleted__roomName__room', { roomName: msg.msg, }); break; case 'user-removed-room-from-team': - messageObject.msg = TAPi18n.__('Removed__roomName__from_this_team', { + messageObject.msg = TAPi18n.__('Removed__roomName__from_the_team', { roomName: msg.msg, }); break; case 'ujt': - messageObject.msg = TAPi18n.__('User_joined_team'); + messageObject.msg = TAPi18n.__('User_joined_the_team'); break; case 'au': - messageObject.msg = TAPi18n.__('User_added_by', { + messageObject.msg = TAPi18n.__('User_added_to', { user_added: hideUserName(msg.msg, userData, usersMap), user_by: username, }); break; case 'added-user-to-team': - messageObject.msg = TAPi18n.__('Added__username__to_team', { + messageObject.msg = TAPi18n.__('Added__username__to_this_team', { user_added: msg.msg, }); break; case 'r': - messageObject.msg = TAPi18n.__('Room_name_changed', { + messageObject.msg = TAPi18n.__('Room_name_changed_to', { room_name: msg.msg, user_by: username, }); break; case 'ru': - messageObject.msg = TAPi18n.__('User_removed_by', { + messageObject.msg = TAPi18n.__('User_has_been_removed', { user_removed: hideUserName(msg.msg, userData, usersMap), user_by: username, }); break; case 'removed-user-from-team': - messageObject.msg = TAPi18n.__('Removed__username__from_team', { + messageObject.msg = TAPi18n.__('Removed__username__from_the_team', { user_removed: hideUserName(msg.msg, userData, usersMap), }); break; diff --git a/apps/meteor/client/startup/messageTypes.ts b/apps/meteor/client/startup/messageTypes.ts index fc087ebc412..6b4d0d104b2 100644 --- a/apps/meteor/client/startup/messageTypes.ts +++ b/apps/meteor/client/startup/messageTypes.ts @@ -9,10 +9,9 @@ Meteor.startup(() => { MessageTypes.registerType({ id: 'room_changed_privacy', system: true, - message: 'room_changed_privacy', + message: 'room_changed_type', data(message: IMessage) { return { - user_by: message.u?.username, room_type: t(message.msg), }; }, @@ -21,10 +20,9 @@ Meteor.startup(() => { MessageTypes.registerType({ id: 'room_changed_topic', system: true, - message: 'room_changed_topic', + message: 'room_changed_topic_to', data(message: IMessage) { return { - user_by: message.u?.username, room_topic: escapeHTML(message.msg || `(${t('None').toLowerCase()})`), }; }, @@ -33,12 +31,7 @@ Meteor.startup(() => { MessageTypes.registerType({ id: 'room_changed_avatar', system: true, - message: 'room_changed_avatar', - data(message: IMessage) { - return { - user_by: message.u?.username, - }; - }, + message: 'room_avatar_changed', }); MessageTypes.registerType({ diff --git a/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts b/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts index 8ef8f31b55a..7eb1af6fd79 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts +++ b/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts @@ -25,7 +25,7 @@ const baseMessage = { MessageTypes.registerType({ id: 'au', system: true, - message: 'User_added_by', + message: 'User_added_to', }); describe('isUserMessage', () => { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 768c66b0773..4a0b449cc8b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -11,6 +11,8 @@ "__username__was_set__role__by__user_by_": "__username__ was set __role__ by __user_by__", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by __username__", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by __username__", + "Enabled_E2E_Encryption_for_this_room": "enabled E2E Encryption for this room", + "Disabled_E2E_Encryption_for_this_room": "disabled E2E Encryption for this room", "@username": "@username", "@username_message": "@username <message>", "#channel": "#channel", @@ -301,6 +303,8 @@ "add-user-to-joined-room_description": "Permission to add a user to a currently joined channel", "added__roomName__to_team": "<strong>added</strong> #__roomName__ to this Team", "Added__username__to_team": "<strong>added</strong> @__user_added__ to this Team", + "added__roomName__to_this_team": "added #__roomName__ to this team", + "Added__username__to_this_team": "added @__user_added__ to this team", "Adding_OAuth_Services": "Adding OAuth Services", "Adding_permission": "Adding permission", "Adding_user": "Adding user", @@ -1095,6 +1099,8 @@ "Converting_channel_to_a_team": "You are converting this Channel to a Team. All members will be kept.", "Converted__roomName__to_team": "<strong>converted</strong> #__roomName__ to a Team", "Converted__roomName__to_channel": "<strong>converted</strong> #__roomName__ to a Channel", + "Converted__roomName__to_a_team": "converted #__roomName__ to a team", + "Converted__roomName__to_a_channel": "converted #__roomName__ to channel", "Converting_team_to_channel": "Converting Team to Channel", "Copied": "Copied", "Copy": "Copy", @@ -1501,6 +1507,7 @@ "delete-user_description": "Permission to delete users", "Deleted": "Deleted!", "Deleted__roomName__": "<strong>deleted</strong> #__roomName__", + "Deleted__roomName__room": "deleted #__roomName__", "Department": "Department", "Department_name": "Department name", "Department_not_found": "Department not found", @@ -3218,7 +3225,8 @@ "Message_Read_Receipt_Enabled": "Show Read Receipts", "Message_Read_Receipt_Store_Users": "Detailed Read Receipts", "Message_Read_Receipt_Store_Users_Description": "Shows each user's read receipts", - "Message_removed": "Message removed", + "Message_removed": "message removed", + "Message_is_removed": "message removed", "Message_sent_by_email": "Message sent by Email", "Message_ShowDeletedStatus": "Show Deleted Status", "Message_ShowEditedStatus": "Show Edited Status", @@ -3707,6 +3715,7 @@ "Privacy_policy": "Privacy policy", "Privacy_summary": "Privacy summary", "Private": "Private", + "private": "private", "Private_Channel": "Private Channel", "Private_Channels": "Private Channels", "Private_Chats": "Private Chats", @@ -3736,6 +3745,7 @@ "Pruning_files": "Pruning files...", "Pruning_messages": "Pruning messages...", "Public": "Public", + "public": "public", "Public_Channel": "Public Channel", "Public_Channels": "Public Channels", "Public_Community": "Public Community", @@ -3867,6 +3877,8 @@ "Removed_User": "Removed User", "Removed__roomName__from_this_team": "<strong>removed</strong> #__roomName__ from this Team", "Removed__username__from_team": "<strong>removed</strong> @__user_removed__ from this Team", + "Removed__roomName__from_the_team": "removed #__roomName__ from this team", + "Removed__username__from_the_team": "removed @__user_removed__ from this team", "Replay": "Replay", "Replied_on": "Replied on", "Replies": "Replies", @@ -3979,6 +3991,7 @@ "Role_removed": "Role removed", "Room": "Room", "room_allowed_reacting": "Room allowed reacting by __user_by__", + "room_allowed_reactions": "allowed reactions", "Room_announcement_changed_successfully": "Room announcement changed successfully", "Room_archivation_state": "State", "Room_archivation_state_false": "Active", @@ -3986,12 +3999,16 @@ "Room_archived": "Room archived", "room_changed_announcement": "Room announcement changed to: __room_announcement__ by __user_by__", "room_changed_avatar": "Room avatar changed by __user_by__", + "room_avatar_changed": "changed room avatar", "room_changed_description": "Room description changed to: __room_description__ by __user_by__", "room_changed_privacy": "Room type changed to: __room_type__ by __user_by__", "room_changed_topic": "Room topic changed to: __room_topic__ by __user_by__", + "room_changed_type": "changed room to __room_type__", + "room_changed_topic_to": "changed room topic to __room_topic__", "Room_default_change_to_private_will_be_default_no_more": "This is a default channel and changing it to a private group will cause it to no longer be a default channel. Do you want to proceed?", "Room_description_changed_successfully": "Room description changed successfully", "room_disallowed_reacting": "Room disallowed reacting by __user_by__", + "room_disallowed_reactions": "disallowed reactions", "Room_Edit": "Room Edit", "Room_has_been_archived": "Room has been archived", "Room_has_been_deleted": "Room has been deleted", @@ -4003,12 +4020,15 @@ "room_is_read_only": "This room is read only", "room_name": "room name", "Room_name_changed": "Room name changed to: __room_name__ by __user_by__", + "Room_name_changed_to": "changed room name to __room_name__", "Room_name_changed_successfully": "Room name changed successfully", "Room_not_exist_or_not_permission": "The room does not exist or you may not have access permission", "Room_not_found": "Room not found", "Room_password_changed_successfully": "Room password changed successfully", "room_removed_read_only": "Room added writing permission by __user_by__", "room_set_read_only": "Room set as Read Only by __user_by__", + "room_removed_read_only_permission": "removed read only permission", + "room_set_read_only_permission": "set room to read only", "Room_topic_changed_successfully": "Room topic changed successfully", "Room_type_changed_successfully": "Room type changed successfully", "Room_type_of_default_rooms_cant_be_changed": "This is a default room and the type can not be changed, please consult with your administrator.", @@ -4650,6 +4670,8 @@ "This_month": "This Month", "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", + "This_room_has_been_archived": "archived room", + "This_room_has_been_unarchived": "unarchived room", "This_week": "This Week", "thread": "thread", "Thread_message": "Commented on *__username__'s* message: _ __msg__ _", @@ -4863,6 +4885,7 @@ "User__username__unmuted_in_room__roomName__": "User __username__ unmuted in room __roomName__", "User_added": "User added", "User_added_by": "User __user_added__ added by __user_by__.", + "User_added_to": "added __user_added__", "User_added_successfully": "User added successfully", "User_and_group_mentions_only": "User and group mentions only", "User_cant_be_empty": "User cannot be empty", @@ -4887,28 +4910,36 @@ "User_joined_channel": "Has joined the channel.", "User_joined_conversation": "Has joined the conversation", "User_joined_team": "<strong>joined</strong> this Team", + "User_joined_the_channel": "joined the channel", + "User_joined_the_conversation": "joined the conversation", + "User_joined_the_team": "joined this team", "user_joined_otr": "Has joined OTR chat.", "user_key_refreshed_successfully": "key refreshed successfully", "user_requested_otr_key_refresh": "Has requested key refresh.", "User_left": "Has left the channel.", "User_left_team": "<strong>left</strong> this Team", + "User_left_this_channel": "left the channel", + "User_left_this_team": "left this team", "User_logged_out": "User is logged out", "User_management": "User Management", "User_mentions_only": "User mentions only", "User_muted": "User Muted", "User_muted_by": "User __user_muted__ muted by __user_by__.", + "User_has_been_muted": "muted __user_muted__", "User_not_found": "User not found", "User_not_found_or_incorrect_password": "User not found or incorrect password", "User_or_channel_name": "User or channel name", "User_Presence": "User Presence", "User_removed": "User removed", "User_removed_by": "User __user_removed__ removed by __user_by__.", + "User_has_been_removed": "removed __user_removed__", "User_sent_a_message_on_channel": "<strong>__username__</strong> sent a message on <strong>__channel__</strong>", "User_sent_a_message_to_you": "<strong>__username__</strong> sent you a message", "user_sent_an_attachment": "__user__ sent an attachment", "User_Settings": "User Settings", "User_started_a_new_conversation": "__username__ started a new conversation", "User_unmuted_by": "User __user_unmuted__ unmuted by __user_by__.", + "User_has_been_unmuted": "unmuted __user_unmuted__", "User_unmuted_in_room": "User unmuted in room", "User_updated_successfully": "User updated successfully", "User_uploaded_a_file_on_channel": "<strong>__username__</strong> uploaded a file on <strong>__channel__</strong>", -- GitLab From 7acbb2a320bbf8c9b11bc9773e9277a133d08d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Albuquerque?= <albuquerquefabio@icloud.com> Date: Tue, 6 Sep 2022 16:09:56 -0300 Subject: [PATCH 014/107] [IMPROVE] OTR Message (#24297) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .../app/lib/server/functions/sendMessage.js | 6 +++- apps/meteor/app/otr/client/OTRRoom.ts | 24 ++++++------- apps/meteor/app/otr/lib/IOTR.ts | 2 +- apps/meteor/app/otr/server/index.ts | 2 +- .../app/otr/server/methods/updateOTRAck.ts | 8 ++--- .../app/ui-utils/client/lib/RoomManager.ts | 2 +- apps/meteor/client/startup/otr.ts | 34 ++++++++++++------- .../notifications/notifications.module.ts | 10 +++++- .../core-typings/src/IMessage/IMessage.ts | 3 +- 9 files changed, 55 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/sendMessage.js b/apps/meteor/app/lib/server/functions/sendMessage.js index e80b527573f..f067a3ad78a 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.js +++ b/apps/meteor/app/lib/server/functions/sendMessage.js @@ -10,6 +10,7 @@ import { hasPermission } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { parseUrlsInMessage } from './parseUrlsInMessage'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; +import notifications from '../../../notifications/server/lib/Notifications'; /** * IMPORTANT @@ -244,7 +245,10 @@ export const sendMessage = function (user, message, room, upsert = false) { message = callbacks.run('beforeSaveMessage', message, room); if (message) { - if (message._id && upsert) { + if (message.t === 'otr') { + const otrStreamer = notifications.streamRoomMessage; + otrStreamer.emit(message.rid, message, user, room); + } else if (message._id && upsert) { const { _id } = message; delete message._id; Messages.upsert( diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index f722a6191cc..57c4097c844 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -66,8 +66,7 @@ export class OTRRoom implements IOTRRoom { } setState(nextState: OtrRoomState): void { - const currentState = this.state.get(); - if (currentState === nextState) { + if (this.getState() === nextState) { return; } @@ -141,7 +140,7 @@ export class OTRRoom implements IOTRRoom { this._userOnlineComputation = Tracker.autorun(() => { const $room = $(`#chat-window-${this._roomId}`); const $title = $('.rc-header__title', $room); - if (this.state.get() === OtrRoomState.ESTABLISHED) { + if (this.getState() === OtrRoomState.ESTABLISHED) { if ($room.length && $title.length && !$('.otr-icon', $title).length) { $title.prepend("<i class='otr-icon icon-key'></i>"); } @@ -252,8 +251,8 @@ export class OTRRoom implements IOTRRoom { const establishConnection = async (): Promise<void> => { this.setState(OtrRoomState.ESTABLISHING); Meteor.clearTimeout(timeout); - try { + if (!data.publicKey) throw new Error('Public key is not generated'); await this.generateKeyPair(); await this.importPublicKey(data.publicKey); await goToRoomById(data.roomId); @@ -283,11 +282,11 @@ export class OTRRoom implements IOTRRoom { throw new Meteor.Error('user-not-defined', 'User not defined.'); } - if (data.refresh && this.state.get() === OtrRoomState.ESTABLISHED) { + if (data.refresh && this.getState() === OtrRoomState.ESTABLISHED) { this.reset(); await establishConnection(); } else { - if (this.state.get() === OtrRoomState.ESTABLISHED) { + if (this.getState() === OtrRoomState.ESTABLISHED) { this.reset(); } imperativeModal.open({ @@ -308,11 +307,11 @@ export class OTRRoom implements IOTRRoom { }, }, }); + timeout = Meteor.setTimeout(() => { + this.setState(OtrRoomState.TIMEOUT); + imperativeModal.close(); + }, 10000); } - timeout = Meteor.setTimeout(() => { - this.setState(OtrRoomState.TIMEOUT); - imperativeModal.close(); - }, 10000); } catch (e) { dispatchToastMessage({ type: 'error', message: e }); } @@ -320,6 +319,7 @@ export class OTRRoom implements IOTRRoom { case 'acknowledge': try { + if (!data.publicKey) throw new Error('Public key is not generated'); await this.importPublicKey(data.publicKey); this.setState(OtrRoomState.ESTABLISHED); @@ -334,7 +334,7 @@ export class OTRRoom implements IOTRRoom { break; case 'deny': - if (this.state.get() === OtrRoomState.ESTABLISHING) { + if (this.getState() === OtrRoomState.ESTABLISHING) { this.reset(); this.setState(OtrRoomState.DECLINED); } @@ -347,7 +347,7 @@ export class OTRRoom implements IOTRRoom { throw new Meteor.Error('user-not-defined', 'User not defined.'); } - if (this.state.get() === OtrRoomState.ESTABLISHED) { + if (this.getState() === OtrRoomState.ESTABLISHED) { this.reset(); this.setState(OtrRoomState.NOT_STARTED); imperativeModal.open({ diff --git a/apps/meteor/app/otr/lib/IOTR.ts b/apps/meteor/app/otr/lib/IOTR.ts index 33548be6a43..da595114a9d 100644 --- a/apps/meteor/app/otr/lib/IOTR.ts +++ b/apps/meteor/app/otr/lib/IOTR.ts @@ -5,8 +5,8 @@ import type { OTRRoom } from '../client/OTRRoom'; export interface IOnUserStreamData { roomId: IRoom['_id']; - publicKey: string; userId: IUser['_id']; + publicKey?: string; refresh?: boolean; } diff --git a/apps/meteor/app/otr/server/index.ts b/apps/meteor/app/otr/server/index.ts index 8d489ec9c16..3657e7d2393 100644 --- a/apps/meteor/app/otr/server/index.ts +++ b/apps/meteor/app/otr/server/index.ts @@ -1,4 +1,4 @@ import './settings'; -import './methods/deleteOldOTRMessages'; import './methods/updateOTRAck'; import './methods/sendSystemMessages'; +import './methods/deleteOldOTRMessages'; diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index b4444ac5656..1eff9d8638e 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -1,13 +1,13 @@ -import type { IMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import { Messages } from '../../../models/server'; +import notifications from '../../../notifications/server/lib/Notifications'; Meteor.methods({ - updateOTRAck(_id: IMessage['_id'], ack: IMessage['otrAck']): void { + updateOTRAck({ message, ack }) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); } - Messages.updateOTRAck(_id, ack); + const otrStreamer = notifications.streamRoomMessage; + otrStreamer.emit(message.rid, { ...message, otr: { ack } }); }, }); diff --git a/apps/meteor/app/ui-utils/client/lib/RoomManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomManager.ts index 85f281f9440..3ba6756212c 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomManager.ts @@ -193,7 +193,7 @@ const computation = Tracker.autorun(() => { } } - msg.name = room.name; + msg.name = room.name || ''; handleTrackSettingsChange(msg); diff --git a/apps/meteor/client/startup/otr.ts b/apps/meteor/client/startup/otr.ts index f10d1a298dd..e63b76275d5 100644 --- a/apps/meteor/client/startup/otr.ts +++ b/apps/meteor/client/startup/otr.ts @@ -1,23 +1,32 @@ -import { AtLeast, IMessage } from '@rocket.chat/core-typings'; +import { IMessage, IRoom, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { Notifications } from '../../app/notifications/client'; import OTR from '../../app/otr/client/OTR'; -import { IOnUserStreamData, IOTRDecrypt } from '../../app/otr/lib/IOTR'; import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; import { t } from '../../app/utils/client'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; import { onClientMessageReceived } from '../lib/onClientMessageReceived'; +type NotifyUserData = { + roomId: IRoom['_id']; + userId: IUser['_id']; +}; + Meteor.startup(() => { Tracker.autorun(() => { if (Meteor.userId()) { - Notifications.onUser('otr', (type: string, data: IOnUserStreamData) => { + Notifications.onUser('otr', (type: string, data: NotifyUserData) => { + if (!data.roomId || !data.userId || data.userId === Meteor.userId()) { + return; + } const instanceByRoomId = OTR.getInstanceByRoomId(data.roomId); - if (!data.roomId || !data.userId || data.userId === Meteor.userId() || !instanceByRoomId) { + + if (!instanceByRoomId) { return; } + instanceByRoomId.onUserStream(type, data); }); } @@ -37,33 +46,32 @@ Meteor.startup(() => { const instanceByRoomId = OTR.getInstanceByRoomId(message.rid); if (message.rid && instanceByRoomId && instanceByRoomId.getState() === OtrRoomState.ESTABLISHED) { - if (message.notification) { + if (message?.notification) { message.msg = t('Encrypted_message'); return message; } - if (message?.t !== 'otr') { + if (message.t !== 'otr') { return message; } - const otrRoom = instanceByRoomId; - const decrypted = await otrRoom.decrypt(message.msg); + + const decrypted = await instanceByRoomId.decrypt(message.msg); if (typeof decrypted === 'string') { return { ...message, msg: decrypted }; } - const { _id, text: msg, ack, ts, userId }: IOTRDecrypt = decrypted; + const { _id, text: msg, ack, ts, userId } = decrypted; if (ts) message.ts = ts; if (message.otrAck) { - const otrAck = await otrRoom.decrypt(message.otrAck); + const otrAck = await instanceByRoomId.decrypt(message.otrAck); if (typeof otrAck === 'string') { return { ...message, msg: otrAck }; } - if (ack === otrAck.text) message.t = 'otr-ack'; } else if (userId !== Meteor.userId()) { - const encryptedAck = await otrRoom.encryptText(ack); + const encryptedAck = await instanceByRoomId.encryptText(ack); - Meteor.call('updateOTRAck', _id, encryptedAck); + Meteor.call('updateOTRAck', { message, ack: encryptedAck }); } return { ...message, _id, msg }; diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index 5da8d00d757..e2243c0fccb 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -282,6 +282,10 @@ export class NotificationsModule { this.streamUser.allowWrite(async function (eventName: string, data: unknown) { const [, e] = eventName.split('/'); + if (e === 'otr' && (data === 'handshake' || data === 'acknowledge')) { + const isEnable = await Settings.getValueById('OTR_Enable'); + return Boolean(this.userId) && (isEnable === 'true' || isEnable === true); + } if (e === 'webrtc') { return true; } @@ -308,11 +312,15 @@ export class NotificationsModule { this.streamUser.allowRead(async function (eventName) { const [userId, e] = eventName.split('/'); + if (e === 'otr') { + const isEnable = await Settings.getValueById('OTR_Enable'); + return Boolean(this.userId) && this.userId === userId && (isEnable === 'true' || isEnable === true); + } if (e === 'webrtc') { return true; } - return this.userId != null && this.userId === userId; + return Boolean(this.userId) && this.userId === userId; }); this.streamImporters.allowRead('all'); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 6b12e428b5d..44bc4f48600 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -57,12 +57,11 @@ type OmnichannelTypesValues = | 'omnichannel_on_hold_chat_resumed'; type OtrMessageTypeValues = 'otr' | 'otr-ack'; + type OtrSystemMessages = 'user_joined_otr' | 'user_requested_otr_key_refresh' | 'user_key_refreshed_successfully'; export type MessageTypesValues = | 'e2e' - | 'otr' - | 'otr-ack' | 'uj' | 'ul' | 'ru' -- GitLab From f8b62b12f65676675eba2f8cf3dc88d6ce1e0325 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 7 Sep 2022 01:41:42 +0530 Subject: [PATCH 015/107] [IMPROVE] Allow delete attachment description on message edit (#26673) Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Co-authored-by: Hugo Costa <20212776+hugocostadev@users.noreply.github.com> --- apps/meteor/app/lib/server/functions/updateMessage.ts | 11 +++++++++-- apps/meteor/app/lib/server/methods/updateMessage.js | 5 ++++- apps/meteor/app/ui/client/lib/ChatMessages.ts | 3 +-- apps/meteor/client/methods/updateMessage.ts | 9 +++++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 410c7f276f5..d597f1a216e 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -1,5 +1,6 @@ -import { Meteor } from 'meteor/meteor'; import type { IMessage, IMessageEdited, IUser } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; +import type { UpdateFilter } from 'mongodb'; import { Messages, Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; @@ -46,8 +47,14 @@ export const updateMessage = function (message: IMessage, user: IUser, originalM message = callbacks.run('beforeSaveMessage', message); const { _id, ...editedMessage } = message; + const unsetData: Partial<UpdateFilter<IMessage>> = {}; + + if (!editedMessage.msg) { + unsetData.md = 1; + delete editedMessage.md; + } - Messages.update({ _id }, { $set: editedMessage }); + Messages.update({ _id }, { $set: editedMessage, $unset: unsetData }); const room = Rooms.findOneById(message.rid); diff --git a/apps/meteor/app/lib/server/methods/updateMessage.js b/apps/meteor/app/lib/server/methods/updateMessage.js index a615d679c52..d3c11f06e53 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.js +++ b/apps/meteor/app/lib/server/methods/updateMessage.js @@ -20,7 +20,10 @@ Meteor.methods({ if (!originalMessage || !originalMessage._id) { return; } - if (originalMessage.msg === message.msg) { + + const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; + + if (msgText === message.msg) { return; } diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index a87155a6a2e..1aa1c80b5c2 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -336,10 +336,9 @@ export class ChatMessages { if (!message) { throw new Error('Message not found'); } - const isDescription = message.attachments?.[0]?.description; try { - if (isDescription) { + if (message.attachments && message.attachments?.length > 0) { // @ts-ignore await this.processMessageEditing({ _id: this.editing.id, rid, msg: '' }); return done(); diff --git a/apps/meteor/client/methods/updateMessage.ts b/apps/meteor/client/methods/updateMessage.ts index 9efe68a452e..ce322684867 100644 --- a/apps/meteor/client/methods/updateMessage.ts +++ b/apps/meteor/client/methods/updateMessage.ts @@ -1,3 +1,4 @@ +import { IEditedMessage } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import moment from 'moment'; @@ -25,7 +26,10 @@ Meteor.methods({ const hasPermission = hasAtLeastOnePermission('edit-message', message.rid); const editAllowed = settings.get('Message_AllowEditing'); let editOwn = false; - if (originalMessage.msg === message.msg) { + + const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; + + if (msgText === message.msg) { return; } if (originalMessage?.u?._id) { @@ -69,7 +73,7 @@ Meteor.methods({ }; message = callbacks.run('beforeSaveMessage', message); - const messageObject = { + const messageObject: Partial<IEditedMessage> = { editedAt: message.editedAt, editedBy: message.editedBy, msg: message.msg, @@ -78,6 +82,7 @@ Meteor.methods({ if (originalMessage.attachments) { if (originalMessage.attachments[0].description !== undefined) { delete messageObject.msg; + originalMessage.attachments[0].description = message.msg; } } ChatMessage.update( -- GitLab From d30e08ecf2d997ce1e96ef51ad53e16da4d3324c Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Thu, 8 Sep 2022 11:39:44 -0300 Subject: [PATCH 016/107] [FIX] Sign in with Apple on mobile (#26827) --- .../app/apple/lib/handleIdentityToken.ts | 45 +++++++++++++ .../app/apple/server/AppleCustomOAuth.ts | 63 ++++--------------- .../apple/server/appleOauthRegisterService.ts | 17 ++++- apps/meteor/app/apple/server/index.ts | 1 + apps/meteor/app/apple/server/loginHandler.ts | 49 +++++++++++++++ .../externals/meteor/accounts-base.d.ts | 6 ++ .../rocketchat-i18n/i18n/en.i18n.json | 3 +- 7 files changed, 132 insertions(+), 52 deletions(-) create mode 100644 apps/meteor/app/apple/lib/handleIdentityToken.ts create mode 100644 apps/meteor/app/apple/server/loginHandler.ts diff --git a/apps/meteor/app/apple/lib/handleIdentityToken.ts b/apps/meteor/app/apple/lib/handleIdentityToken.ts new file mode 100644 index 00000000000..528c8f41683 --- /dev/null +++ b/apps/meteor/app/apple/lib/handleIdentityToken.ts @@ -0,0 +1,45 @@ +import { KJUR } from 'jsrsasign'; +import { HTTP } from 'meteor/http'; +import NodeRSA from 'node-rsa'; + +function isValidAppleJWT(identityToken: string, header: any): boolean { + const applePublicKeys = HTTP.get('https://appleid.apple.com/auth/keys').data.keys as any; + const { kid } = header; + + const key = applePublicKeys.find((k: any) => k.kid === kid); + + const pubKey = new NodeRSA(); + pubKey.importKey({ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') }, 'components-public'); + const userKey = pubKey.exportKey('public'); + + try { + return KJUR.jws.JWS.verify(identityToken, userKey, ['RS256']); + } catch { + return false; + } +} + +export function handleIdentityToken(identityToken: string): { id: string; email: string; name: string } { + const decodedToken = KJUR.jws.JWS.parse(identityToken); + + if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) { + throw new Error('identityToken is not a valid JWT'); + } + + if (!decodedToken.payloadObj) { + throw new Error('identityToken does not have a payload'); + } + + const { iss, sub, email } = decodedToken.payloadObj as any; + if (!iss) { + throw new Error('Insufficient data in auth response token'); + } + + const serviceData = { + id: sub, + email, + name: '', + }; + + return serviceData; +} diff --git a/apps/meteor/app/apple/server/AppleCustomOAuth.ts b/apps/meteor/app/apple/server/AppleCustomOAuth.ts index 01c5027a44f..7779f4f814c 100644 --- a/apps/meteor/app/apple/server/AppleCustomOAuth.ts +++ b/apps/meteor/app/apple/server/AppleCustomOAuth.ts @@ -1,72 +1,35 @@ import { Accounts } from 'meteor/accounts-base'; -import { HTTP } from 'meteor/http'; -import NodeRSA from 'node-rsa'; -import { KJUR } from 'jsrsasign'; import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { MeteorError } from '../../../server/sdk/errors'; - -const isValidAppleJWT = (identityToken: string, header: any): any => { - const applePublicKeys = HTTP.get('https://appleid.apple.com/auth/keys').data.keys as any; - const { kid } = header; - - const key = applePublicKeys.find((k: any) => k.kid === kid); - - const pubKey = new NodeRSA(); - pubKey.importKey({ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') }, 'components-public'); - const userKey = pubKey.exportKey('public'); - - try { - return KJUR.jws.JWS.verify(identityToken, userKey, ['RS256']); - } catch { - return false; - } -}; +import { handleIdentityToken } from '../lib/handleIdentityToken'; export class AppleCustomOAuth extends CustomOAuth { getIdentity(_accessToken: string, query: Record<string, any>): any { const { id_token: identityToken, user: userStr = '' } = query; - let user = {} as any; + let usrObj = {} as any; try { - user = JSON.parse(userStr); + usrObj = JSON.parse(userStr); } catch (e) { // ignore } - const decodedToken = KJUR.jws.JWS.parse(identityToken); + try { + const serviceData = handleIdentityToken(identityToken); - if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) { - return { - type: 'apple', - error: new MeteorError(Accounts.LoginCancelledError.numericError, 'identityToken is a invalid JWT'), - }; - } + if (usrObj?.name) { + serviceData.name = `${usrObj.name.firstName}${usrObj.name.middleName ? ` ${usrObj.name.middleName}` : ''}${ + usrObj.name.lastName ? ` ${usrObj.name.lastName}` : '' + }`; + } - const { iss, sub, email } = decodedToken.payloadObj as any; - if (!iss) { + return serviceData; + } catch (error: any) { return { type: 'apple', - error: new MeteorError(Accounts.LoginCancelledError.numericError, 'Insufficient data in auth response token'), + error: new MeteorError(Accounts.LoginCancelledError.numericError, error.message), }; } - - const serviceData = { - id: sub, - email, - name: '', - }; - - if (email) { - serviceData.email = email; - } - - if (user?.name) { - serviceData.name = `${user.name.firstName}${user.name.middleName ? ` ${user.name.middleName}` : ''}${ - user.name.lastName ? ` ${user.name.lastName}` : '' - }`; - } - - return serviceData; } } diff --git a/apps/meteor/app/apple/server/appleOauthRegisterService.ts b/apps/meteor/app/apple/server/appleOauthRegisterService.ts index c7f01a68091..728daf40105 100644 --- a/apps/meteor/app/apple/server/appleOauthRegisterService.ts +++ b/apps/meteor/app/apple/server/appleOauthRegisterService.ts @@ -34,6 +34,22 @@ settings.watchMultiple( }); } + // if everything is empty but Apple login is enabled, don't show the login button + if (!clientId && !serverSecret && !iss && !kid) { + ServiceConfiguration.configurations.upsert( + { + service: 'apple', + }, + { + $set: { + showButton: false, + enabled: settings.get('Accounts_OAuth_Apple'), + }, + }, + ); + return; + } + const HEADER = { kid, alg: 'ES256', @@ -67,7 +83,6 @@ settings.watchMultiple( enabled: settings.get('Accounts_OAuth_Apple'), loginStyle: 'popup', clientId, - buttonLabelText: 'Sign in with Apple', buttonColor: '#000', buttonLabelColor: '#FFF', }, diff --git a/apps/meteor/app/apple/server/index.ts b/apps/meteor/app/apple/server/index.ts index d4b3d92958a..5643ad67a54 100644 --- a/apps/meteor/app/apple/server/index.ts +++ b/apps/meteor/app/apple/server/index.ts @@ -1 +1,2 @@ import './appleOauthRegisterService'; +import './loginHandler'; diff --git a/apps/meteor/app/apple/server/loginHandler.ts b/apps/meteor/app/apple/server/loginHandler.ts new file mode 100644 index 00000000000..e69ea39b34c --- /dev/null +++ b/apps/meteor/app/apple/server/loginHandler.ts @@ -0,0 +1,49 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; + +import { handleIdentityToken } from '../lib/handleIdentityToken'; +import { settings } from '../../settings/server'; + +Accounts.registerLoginHandler('apple', (loginRequest) => { + if (!loginRequest.identityToken) { + return; + } + + if (!settings.get('Accounts_OAuth_Apple')) { + return; + } + + const { identityToken, fullName, email } = loginRequest; + + try { + const serviceData = handleIdentityToken(identityToken); + + if (!serviceData.email && email) { + serviceData.email = email; + } + + const profile: { name?: string } = {}; + + const { givenName, familyName } = fullName; + if (givenName && familyName) { + profile.name = `${givenName} ${familyName}`; + } + + const result = Accounts.updateOrCreateUserFromExternalService('apple', serviceData, { profile }); + + // Ensure processing succeeded + if (result === undefined || result.userId === undefined) { + return { + type: 'apple', + error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Apple response token'), + }; + } + + return result; + } catch (error: any) { + return { + type: 'apple', + error: new Meteor.Error(Accounts.LoginCancelledError.numericError, error.message), + }; + } +}); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 6cd2a9e8e9e..2185629b516 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -23,6 +23,12 @@ declare module 'meteor/accounts-base' { function _runLoginHandlers<T>(methodInvocation: T, loginRequest: Record<string, any>): Record<string, any> | undefined; + function updateOrCreateUserFromExternalService( + serviceName: string, + serviceData: Record<string, unknown>, + options: Record<string, unknown>, + ): Record<string, unknown>; + export class ConfigError extends Error {} export class LoginCancelledError extends Error { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 4a0b449cc8b..1f8c8aefa11 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -103,6 +103,7 @@ "Accounts_LoginExpiration": "Login Expiration in Days", "Accounts_ManuallyApproveNewUsers": "Manually Approve New Users", "Accounts_OAuth_Apple": "Sign in with Apple", + "Accounts_OAuth_Apple_Description": "If you want Apple login enabled only on mobile, you can leave all fields empty.", "Accounts_OAuth_Custom_Access_Token_Param": "Param Name for access token", "Accounts_OAuth_Custom_Authorize_Path": "Authorize Path", "Accounts_OAuth_Custom_Avatar_Field": "Avatar field", @@ -5421,4 +5422,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Login Detected", "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", "Something_Went_Wrong": "Something went wrong" -} \ No newline at end of file +} -- GitLab From 343c5f1824a5f74fa97b92ad4700912a3ec9d652 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Thu, 8 Sep 2022 20:25:45 -0300 Subject: [PATCH 017/107] Chore: add aria hidden if modal is open (#26826) --- .../meteor/client/views/modal/ModalRegion.tsx | 7 +- .../root/MainLayout/LayoutWithSidebar.tsx | 10 +- apps/meteor/package.json | 1 + yarn.lock | 1926 +++++++++++++++-- 4 files changed, 1741 insertions(+), 203 deletions(-) diff --git a/apps/meteor/client/views/modal/ModalRegion.tsx b/apps/meteor/client/views/modal/ModalRegion.tsx index 928683dbd87..cb7da04b3cf 100644 --- a/apps/meteor/client/views/modal/ModalRegion.tsx +++ b/apps/meteor/client/views/modal/ModalRegion.tsx @@ -1,5 +1,6 @@ import { useModal, useCurrentModal } from '@rocket.chat/ui-contexts'; import React, { useCallback, ReactElement } from 'react'; +import { FocusScope } from 'react-aria'; import ModalBackdrop from '../../components/modal/ModalBackdrop'; import ModalPortal from '../../components/modal/ModalPortal'; @@ -15,7 +16,11 @@ const ModalRegion = (): ReactElement | null => { return ( <ModalPortal> - <ModalBackdrop onDismiss={handleDismiss}>{currentModal}</ModalBackdrop> + <ModalBackdrop onDismiss={handleDismiss}> + <FocusScope contain restoreFocus autoFocus> + {currentModal} + </FocusScope> + </ModalBackdrop> </ModalPortal> ); }; diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx index 1d55b533412..9c7f69972d9 100644 --- a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx +++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx @@ -1,4 +1,4 @@ -import { useLayout, useCurrentRoute, useRoutePath, useSetting } from '@rocket.chat/ui-contexts'; +import { useLayout, useCurrentRoute, useRoutePath, useSetting, useCurrentModal } from '@rocket.chat/ui-contexts'; import React, { ReactElement, ReactNode, useCallback } from 'react'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; @@ -7,6 +7,8 @@ import BlazeTemplate from '../BlazeTemplate'; const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement => { const { isEmbedded: embeddedLayout } = useLayout(); const [currentRouteName = '', currentParameters = {}] = useCurrentRoute(); + + const modal = useCurrentModal(); const currentRoutePath = useRoutePath(currentRouteName, currentParameters); const removeSidenav = useReactiveValue( useCallback(() => embeddedLayout && !currentRoutePath?.startsWith('/admin'), [currentRoutePath, embeddedLayout]), @@ -14,7 +16,11 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement const readReceiptsEnabled = useSetting('Message_Read_Receipt_Store_Users'); return ( - <div id='rocket-chat' className={[embeddedLayout ? 'embedded-view' : undefined, 'menu-nav'].filter(Boolean).join(' ')}> + <div + id='rocket-chat' + className={[embeddedLayout ? 'embedded-view' : undefined, 'menu-nav'].filter(Boolean).join(' ')} + aria-hidden={Boolean(modal)} + > {!removeSidenav ? <BlazeTemplate template='sideNav' /> : null} <div className={['rc-old', 'main-content', 'content-background-color', readReceiptsEnabled ? 'read-receipts-enabled' : undefined] diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c4b9da17759..6426511a827 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -344,6 +344,7 @@ "queue-fifo": "^0.2.6", "rc-scrollbars": "^1.1.5", "react": "~17.0.2", + "react-aria": "^3.19.0", "react-dom": "~17.0.2", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.30.0", diff --git a/yarn.lock b/yarn.lock index b2d57ae5aa9..d85300fb7f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1570,6 +1570,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.6.2": + version: 7.19.0 + resolution: "@babel/runtime@npm:7.19.0" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: fa69c351bb05e1db3ceb9a02fdcf620c234180af68cdda02152d3561015f6d55277265d3109815992f96d910f3db709458cae4f8df1c3def66f32e0867d82294 + languageName: node + linkType: hard + "@babel/runtime@npm:~7.5.4": version: 7.5.5 resolution: "@babel/runtime@npm:7.5.5" @@ -1973,6 +1982,55 @@ __metadata: languageName: node linkType: hard +"@formatjs/ecma402-abstract@npm:1.12.0": + version: 1.12.0 + resolution: "@formatjs/ecma402-abstract@npm:1.12.0" + dependencies: + "@formatjs/intl-localematcher": 0.2.31 + tslib: 2.4.0 + checksum: 29dc157d669f4fe267b850d06ae2c5a9b666a2b859ba1c99a8228bb10e9b2d7cbc19fdf0e247efed6f5100fd33333cecfb0e86315b52fad639cb137aef44b367 + languageName: node + linkType: hard + +"@formatjs/fast-memoize@npm:1.2.6": + version: 1.2.6 + resolution: "@formatjs/fast-memoize@npm:1.2.6" + dependencies: + tslib: 2.4.0 + checksum: cdb944a9207b5d74e0b4cdcd047e32d904b52b8f893227809a906f65882a46ae8b342872161d797dffd4fafd565f91efebb18989ffe888786bb5e5d911bc0193 + languageName: node + linkType: hard + +"@formatjs/icu-messageformat-parser@npm:2.1.7": + version: 2.1.7 + resolution: "@formatjs/icu-messageformat-parser@npm:2.1.7" + dependencies: + "@formatjs/ecma402-abstract": 1.12.0 + "@formatjs/icu-skeleton-parser": 1.3.13 + tslib: 2.4.0 + checksum: 4a7e7b3628852c2379bd30b540c87fd1a84d0878ddd221b7b0fbad317263626d4ba063bf1be104aa9779bad3b819cfaf41f51cda0573787bdbea7acc607025cf + languageName: node + linkType: hard + +"@formatjs/icu-skeleton-parser@npm:1.3.13": + version: 1.3.13 + resolution: "@formatjs/icu-skeleton-parser@npm:1.3.13" + dependencies: + "@formatjs/ecma402-abstract": 1.12.0 + tslib: 2.4.0 + checksum: 8d52b4da2e25b1ab79300da1e7026b740467d3e66e99ae61cf7b6e890dc4a5790ee9c66944319a3f7a74d3e2807c81fa8573e7d33337311ffd9128b90d03c8c7 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.2.31": + version: 0.2.31 + resolution: "@formatjs/intl-localematcher@npm:0.2.31" + dependencies: + tslib: 2.4.0 + checksum: c05bf5854f04ad0cc5ad78436023805c9542d97cdf000c685793e2053b84b585be3603b370e27921a617bbb87ef021239d773bc5326ab99850786c73d46a5156 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -2082,6 +2140,43 @@ __metadata: languageName: node linkType: hard +"@internationalized/date@npm:^3.0.1": + version: 3.0.1 + resolution: "@internationalized/date@npm:3.0.1" + dependencies: + "@babel/runtime": ^7.6.2 + checksum: ff51a00550322a5df3d3051e8ffdf3d7741851149e8ba300883e01402249602e87cc50b27b972753d9af88c5374df83c24adf58cae5e269100cb946a3b12cd56 + languageName: node + linkType: hard + +"@internationalized/message@npm:^3.0.9": + version: 3.0.9 + resolution: "@internationalized/message@npm:3.0.9" + dependencies: + "@babel/runtime": ^7.6.2 + intl-messageformat: ^10.1.0 + checksum: b3f7f5a8e1d8df99efb3463ca07edb976ecf95d28de19a47d92fb19c093052b1a092aeaa226dc69d07143854bdbeb8519a0ac8ba8c900c4b0f565151d735ca7f + languageName: node + linkType: hard + +"@internationalized/number@npm:^3.1.1": + version: 3.1.1 + resolution: "@internationalized/number@npm:3.1.1" + dependencies: + "@babel/runtime": ^7.6.2 + checksum: 9979ea1ca7388de75193c9d36f19d928fbcb715d456d153c30cafadd2ce1ceae011f55c966d424f4561ec04de14d3b48b8fe16a9e2737273829a813c4f7203a3 + languageName: node + linkType: hard + +"@internationalized/string@npm:^3.0.0": + version: 3.0.0 + resolution: "@internationalized/string@npm:3.0.0" + dependencies: + "@babel/runtime": ^7.6.2 + checksum: fc347cf80cd4ee009d1c467dca2c6908a919ad152086bf5e8c1a0aede0383fb317695fc5d82abe033ec90ad62108297130b653b63b9529f2e032999798ae4a81 + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -2871,287 +2966,1655 @@ __metadata: d3-delaunay: ^5.3.0 d3-scale: ^3.2.3 peerDependencies: - "@nivo/core": 0.79.0 - react: ">= 16.14.0 < 18.0.0" - checksum: d479051850a519c328ce8dd6fbbc5998575cb2fc16351e7e516593be5ad21de0f7effe18ad09a2d596abcd90c055a560a21e125b7c8b2a859a309e7737d028e1 + "@nivo/core": 0.79.0 + react: ">= 16.14.0 < 18.0.0" + checksum: d479051850a519c328ce8dd6fbbc5998575cb2fc16351e7e516593be5ad21de0f7effe18ad09a2d596abcd90c055a560a21e125b7c8b2a859a309e7737d028e1 + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: ^1.1.9 + checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:^1.1.2": + version: 1.1.3 + resolution: "@nodelib/fs.stat@npm:1.1.3" + checksum: 318deab369b518a34778cdaa0054dd28a4381c0c78e40bbd20252f67d084b1d7bf9295fea4423de2c19ac8e1a34f120add9125f481b2a710f7068bcac7e3e305 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: ^1.6.0 + checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^1.0.0": + version: 1.1.1 + resolution: "@npmcli/fs@npm:1.1.1" + dependencies: + "@gar/promisify": ^1.0.1 + semver: ^7.3.5 + checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^2.1.0": + version: 2.1.0 + resolution: "@npmcli/fs@npm:2.1.0" + dependencies: + "@gar/promisify": ^1.1.3 + semver: ^7.3.5 + checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b + languageName: node + linkType: hard + +"@npmcli/move-file@npm:^1.0.1, @npmcli/move-file@npm:^1.1.2": + version: 1.1.2 + resolution: "@npmcli/move-file@npm:1.1.2" + dependencies: + mkdirp: ^1.0.4 + rimraf: ^3.0.2 + checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7 + languageName: node + linkType: hard + +"@opencensus/core@npm:0.0.9": + version: 0.0.9 + resolution: "@opencensus/core@npm:0.0.9" + dependencies: + continuation-local-storage: ^3.2.1 + log-driver: ^1.2.7 + semver: ^5.5.0 + shimmer: ^1.2.0 + uuid: ^3.2.1 + checksum: 37af50394cbf759a835182eedf9fbf328232916bdd2174e75221cf9f29393621d0d05eaf24bb61b89b50c740bc4f625e5c8f891e2db627ce6dbf1888d98857e5 + languageName: node + linkType: hard + +"@opencensus/core@npm:^0.0.8": + version: 0.0.8 + resolution: "@opencensus/core@npm:0.0.8" + dependencies: + continuation-local-storage: ^3.2.1 + log-driver: ^1.2.7 + semver: ^5.5.0 + shimmer: ^1.2.0 + uuid: ^3.2.1 + checksum: b4ad6c9a4963407195ab9c2f2751888b1462807c547bd9a7ceb561c473073aacc17555afa9e37cbda01805be5e954085284629e502ed5d3fe7b3c628d9ec2882 + languageName: node + linkType: hard + +"@opencensus/propagation-b3@npm:0.0.8": + version: 0.0.8 + resolution: "@opencensus/propagation-b3@npm:0.0.8" + dependencies: + "@opencensus/core": ^0.0.8 + uuid: ^3.2.1 + checksum: c480bd181062ac15f23d5c833a1b1760f2734e013ab7d0bc3fc24d7d1f8330f38f426a56df6d1baf1384727b3dbc1f9ba2851caaac2125500327117fbafa8f70 + languageName: node + linkType: hard + +"@playwright/test@npm:^1.22.2": + version: 1.23.1 + resolution: "@playwright/test@npm:1.23.1" + dependencies: + "@types/node": "*" + playwright-core: 1.23.1 + bin: + playwright: cli.js + checksum: 27fc6b4424dfb0f0dfaaecb01a33bf77c0f95e6a1e635fe85dd4ad36e5053afdbe4ad1de4be1c5705e482451b43f7ddacf667f6c5b70ab5f8e32c30110211c29 + languageName: node + linkType: hard + +"@pm2/agent@npm:~2.0.0": + version: 2.0.1 + resolution: "@pm2/agent@npm:2.0.1" + dependencies: + async: ~3.2.0 + chalk: ~3.0.0 + dayjs: ~1.8.24 + debug: ~4.3.1 + eventemitter2: ~5.0.1 + fast-json-patch: ^3.0.0-1 + fclone: ~1.0.11 + nssocket: 0.6.0 + pm2-axon: ~4.0.1 + pm2-axon-rpc: ~0.7.0 + proxy-agent: ~5.0.0 + semver: ~7.2.0 + ws: ~7.4.0 + checksum: d15e1ccec8e25bd020eabf2a59e0d2896082414b7950e918a1a19485ef1001012d7df6063fb720f453d9a2cd887abc80d576f985d2c28c127a7c1a16b95aee65 + languageName: node + linkType: hard + +"@pm2/io@npm:~5.0.0": + version: 5.0.0 + resolution: "@pm2/io@npm:5.0.0" + dependencies: + "@opencensus/core": 0.0.9 + "@opencensus/propagation-b3": 0.0.8 + async: ~2.6.1 + debug: ~4.3.1 + eventemitter2: ^6.3.1 + require-in-the-middle: ^5.0.0 + semver: 6.3.0 + shimmer: ^1.2.0 + signal-exit: ^3.0.3 + tslib: 1.9.3 + checksum: f912096e823003de941a539d1098243cdc39b896704dea176e75a1d4fb0e0573b552c0a30e5198c2415fee9e1d8365ce0a8e78257eb85a8bfb994f1fb8153af1 + languageName: node + linkType: hard + +"@pm2/js-api@npm:~0.6.7": + version: 0.6.7 + resolution: "@pm2/js-api@npm:0.6.7" + dependencies: + async: ^2.6.3 + axios: ^0.21.0 + debug: ~4.3.1 + eventemitter2: ^6.3.1 + ws: ^7.0.0 + checksum: 43d1e4d06328ad18d8929225a0732fa20701694dea0e3af0ea692239e0adc45ef2f48a794599a8df88bc705d2e428b5b8018deb3709983c91ed0b74a834b2f21 + languageName: node + linkType: hard + +"@pm2/pm2-version-check@npm:latest": + version: 1.0.4 + resolution: "@pm2/pm2-version-check@npm:1.0.4" + dependencies: + debug: ^4.3.1 + checksum: 663438d9154db3c11bc7d41a4838162542db9cb4cf989fe8936e9bd9e5b99e3a58d73c8888afa1180a8cb93375c1d165bb397939de8a5ab8507d13d2aed2a086 + languageName: node + linkType: hard + +"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": + version: 0.5.7 + resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7" + dependencies: + ansi-html-community: ^0.0.8 + common-path-prefix: ^3.0.0 + core-js-pure: ^3.8.1 + error-stack-parser: ^2.0.6 + find-up: ^5.0.0 + html-entities: ^2.1.0 + loader-utils: ^2.0.0 + schema-utils: ^3.0.0 + source-map: ^0.7.3 + peerDependencies: + "@types/webpack": 4.x || 5.x + react-refresh: ">=0.10.0 <1.0.0" + sockjs-client: ^1.4.0 + type-fest: ">=0.17.0 <3.0.0" + webpack: ">=4.43.0 <6.0.0" + webpack-dev-server: 3.x || 4.x + webpack-hot-middleware: 2.x + webpack-plugin-serve: 0.x || 1.x + peerDependenciesMeta: + "@types/webpack": + optional: true + sockjs-client: + optional: true + type-fest: + optional: true + webpack-dev-server: + optional: true + webpack-hot-middleware: + optional: true + webpack-plugin-serve: + optional: true + checksum: 3490649181878cc8808fb91f3870ef095e5a1fb9647b3ac83740df07379c9d1cf540f24bf2b09d5f26a3a8c805b2c6b9c5be7192bdb9317d0ffffa67426e9f66 + languageName: node + linkType: hard + +"@react-aria/breadcrumbs@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/breadcrumbs@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/link": ^3.3.3 + "@react-aria/utils": ^3.13.3 + "@react-types/breadcrumbs": ^3.4.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b1c8beba84710321aa17524519a29a3386e39cc1a4ac63a934b5df44b9088482f5652e1038dc57827fc0061893a38d650075c5c96b7ee7ec88fda98999ffe31f + languageName: node + linkType: hard + +"@react-aria/button@npm:^3.6.1": + version: 3.6.1 + resolution: "@react-aria/button@npm:3.6.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/toggle": ^3.4.1 + "@react-types/button": ^3.6.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b7c520e7d7b885314cd3455f7b50cfd47f423740873718a2fed9a5721904cd29673efb210101896464afd9392adfec3cba546d118b8f4e6e84cb9ab7ee4b7018 + languageName: node + linkType: hard + +"@react-aria/calendar@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-aria/calendar@npm:3.0.2" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/date": ^3.0.1 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/calendar": ^3.0.2 + "@react-types/button": ^3.6.1 + "@react-types/calendar": ^3.0.2 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 9dbbedc9f60138491703ae5afde6841e1c9d0ff79af853e68b5aef75c93c1e31ce3447a88bb568b52752ada70e8cb431b3219d8a0481a5bc7398db8e62c09199 + languageName: node + linkType: hard + +"@react-aria/checkbox@npm:^3.5.1": + version: 3.5.1 + resolution: "@react-aria/checkbox@npm:3.5.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/label": ^3.4.1 + "@react-aria/toggle": ^3.3.3 + "@react-aria/utils": ^3.13.3 + "@react-stately/checkbox": ^3.2.1 + "@react-stately/toggle": ^3.4.1 + "@react-types/checkbox": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 5f661f6514dd5e92f622cdb73e98994d571244cad5be3fbfe6512cceced6588b2f86162bc1f40030f35618de73fc4954bb0ef824161b22fceba21ecf22d48bd9 + languageName: node + linkType: hard + +"@react-aria/combobox@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/combobox@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/listbox": ^3.6.1 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/menu": ^3.6.1 + "@react-aria/overlays": ^3.10.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/textfield": ^3.7.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/collections": ^3.4.3 + "@react-stately/combobox": ^3.2.1 + "@react-stately/layout": ^3.7.0 + "@react-types/button": ^3.6.1 + "@react-types/combobox": ^3.5.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: cf42473a6ca7b6ce9f0f2855f2ec49b5af586ab4218686eef256f264c9aa70c4f0a33e180c28fbdac8e32dbfa26a08555e56907a3282fc810a58f5f7bddb9866 + languageName: node + linkType: hard + +"@react-aria/datepicker@npm:^3.1.1": + version: 3.1.1 + resolution: "@react-aria/datepicker@npm:3.1.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/date": ^3.0.1 + "@internationalized/number": ^3.1.1 + "@internationalized/string": ^3.0.0 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/spinbutton": ^3.1.3 + "@react-aria/utils": ^3.13.3 + "@react-stately/datepicker": ^3.0.2 + "@react-types/button": ^3.6.1 + "@react-types/calendar": ^3.0.2 + "@react-types/datepicker": ^3.1.1 + "@react-types/dialog": ^3.4.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ca9982f69213c41c6913ab6ca28fb4f89b2b028367dbfc152c3002a651632631139977a245406fce96a90593fc3b80a62eb808504af0af6522518963f9661bdc + languageName: node + linkType: hard + +"@react-aria/dialog@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/dialog@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/overlays": ^3.4.1 + "@react-types/dialog": ^3.4.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ba924f07a7974b0f9a90902c8b2a41490536194e38b53dc069b4ba275c4a557f4aa4ab87e22e2b1f69d8aa8ef42ede142bf45273bf3e55ff9a38cd687d0ee235 + languageName: node + linkType: hard + +"@react-aria/dnd@npm:3.0.0-alpha.12": + version: 3.0.0-alpha.12 + resolution: "@react-aria/dnd@npm:3.0.0-alpha.12" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/string": ^3.0.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/overlays": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-aria/visually-hidden": ^3.4.1 + "@react-stately/dnd": 3.0.0-alpha.10 + "@react-stately/selection": ^3.10.3 + "@react-types/button": ^3.6.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 2f0fbac27120b408a0acc12241bd2f07b96d24721a5d657f5077ad58f8661db9481bee5959c95b5effd76c317424c65476bc2e4316dc9d0f07eb864bee4bbc9e + languageName: node + linkType: hard + +"@react-aria/focus@npm:^3.8.0": + version: 3.8.0 + resolution: "@react-aria/focus@npm:3.8.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + clsx: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 2250e610c3753d008e01d39bed41d961bf795a4cec8873b76fda0adc3ad48811ae5cad0d2e222cca41c43454666d492e130113533e1609fd3cea8721108863a3 + languageName: node + linkType: hard + +"@react-aria/grid@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/grid@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/grid": ^3.3.1 + "@react-stately/selection": ^3.10.3 + "@react-stately/virtualizer": ^3.3.0 + "@react-types/checkbox": ^3.3.3 + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 42be9ef0e051b4bd6b2936821f877016ac243b32e9e6e18199afd8ef9ca88391531422c07baf904850e6ef9c1b1c94cb17f8d7e0939f3b071bb26f0f98ff8329 + languageName: node + linkType: hard + +"@react-aria/gridlist@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-aria/gridlist@npm:3.0.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/grid": ^3.4.1 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/list": ^3.5.3 + "@react-types/checkbox": ^3.3.3 + "@react-types/list": ^3.0.0 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 83d1b77a57f8fa729b7eafa0148829d3c1e0a54fd41fe8c7390ec4382b078407735e0a4ce9587436118c000ff7d5647788d3aa838f31ed93458475f83b9c1f3c + languageName: node + linkType: hard + +"@react-aria/i18n@npm:^3.6.0": + version: 3.6.0 + resolution: "@react-aria/i18n@npm:3.6.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/date": ^3.0.1 + "@internationalized/message": ^3.0.9 + "@internationalized/number": ^3.1.1 + "@internationalized/string": ^3.0.0 + "@react-aria/ssr": ^3.3.0 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ede9cd611e15fe2975556dfe695bdcb67cbcb8d2dfff7677174f86f1418421491fbbbfd8eab40e724a8db24877d2f980df6e50d26d29d5b3e607ca39b42befc3 + languageName: node + linkType: hard + +"@react-aria/interactions@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-aria/interactions@npm:3.11.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 668658282c937a12d6c9791025d5a672110f9cfa7452d3178fec56cb4b32682fd4d389d44498d788a8619668bb537ce9a8dcd1a6d2ad9fd25aa778dbc5e62bc9 + languageName: node + linkType: hard + +"@react-aria/label@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/label@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/utils": ^3.13.3 + "@react-types/label": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f0dc33a9adde0c411d279a57e5d37c33ad3afa700bb20b3fadd928f2b454f66ba5dbc46e5a2cea2cab84ed507177b87bb3fdd155f029fd8f3ee85c1abcecac0d + languageName: node + linkType: hard + +"@react-aria/link@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-aria/link@npm:3.3.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-types/link": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 119b24885c7251fdeeccd8bb221cadad280c0c7eecf1b5bb8c105a2a5dfe5f2a6a37da31a3b40992503606812baa43f1e0e458176c40ff49cbe143ea33068480 + languageName: node + linkType: hard + +"@react-aria/listbox@npm:^3.6.1": + version: 3.6.1 + resolution: "@react-aria/listbox@npm:3.6.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/collections": ^3.4.3 + "@react-stately/list": ^3.5.3 + "@react-types/listbox": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 674797c6ae46d314a68833c8925f56b07a43c787b49fb9bd37559ddafd5cce0c8e8954904f76af86821599c144a2b295dc3eb6f3e71465f0166390d53abc593d + languageName: node + linkType: hard + +"@react-aria/live-announcer@npm:^3.1.1": + version: 3.1.1 + resolution: "@react-aria/live-announcer@npm:3.1.1" + dependencies: + "@babel/runtime": ^7.6.2 + checksum: feb02fe339ed1ce005b3fc6f07366ea100fbdfc3e42688f52d4e6704f6e09724b37f4e6b0c121578081940af11004421aab1b1a91f99c7193c4c2945ff43f92c + languageName: node + linkType: hard + +"@react-aria/menu@npm:^3.6.1": + version: 3.6.1 + resolution: "@react-aria/menu@npm:3.6.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/overlays": ^3.10.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/collections": ^3.4.3 + "@react-stately/menu": ^3.4.1 + "@react-stately/tree": ^3.3.3 + "@react-types/button": ^3.6.1 + "@react-types/menu": ^3.7.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a2632174aa2abfdd6a4e01430f543b8b7f68b49eb27d29418468364962f438234d5c06b1c37c8cd33da52d1e9c752bb1df9c9c7e8a3938c962ed25f2a8031661 + languageName: node + linkType: hard + +"@react-aria/meter@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/meter@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/progress": ^3.3.1 + "@react-types/meter": ^3.2.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 43a7d473c363f16eddbffb860de62c84eb492c3450103ebfb505758f5eb0c5e1c7a1297f2e7a94e13e4ec8e1315c8330737d65d04a47df7b4822b0d693fdaed4 + languageName: node + linkType: hard + +"@react-aria/numberfield@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/numberfield@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/spinbutton": ^3.1.3 + "@react-aria/textfield": ^3.7.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/numberfield": ^3.2.1 + "@react-types/button": ^3.6.1 + "@react-types/numberfield": ^3.3.3 + "@react-types/shared": ^3.14.1 + "@react-types/textfield": ^3.5.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ec16f467c912dff68ef14626d7fedddb9913c993e14c359447ed65a71a3c53f2a85397abebcc106c13b467e7bbd061723e3641375cc7a89abeea2014fd0f9083 + languageName: node + linkType: hard + +"@react-aria/overlays@npm:^3.10.1": + version: 3.10.1 + resolution: "@react-aria/overlays@npm:3.10.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/ssr": ^3.3.0 + "@react-aria/utils": ^3.13.3 + "@react-aria/visually-hidden": ^3.4.1 + "@react-stately/overlays": ^3.4.1 + "@react-types/button": ^3.6.1 + "@react-types/overlays": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b83ec155d34a2cfe7c26d4b4bd5b620c3895642521717c99212aa0878fb4716cc42665a7f80b844185ee4ee4f7e1a367b42399724fa769079d46f29b1c7b67ef + languageName: node + linkType: hard + +"@react-aria/progress@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/progress@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/label": ^3.4.1 + "@react-aria/utils": ^3.13.3 + "@react-types/progress": ^3.2.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: eafa2e8ecd1766686fa4d0af75bc4be87145e341189efa6400160768ce0ea9e8aa66a3e90269b812c130022bd66a2546213ef74592d3d88a129b5da6beeba181 + languageName: node + linkType: hard + +"@react-aria/radio@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/radio@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/radio": ^3.5.1 + "@react-types/radio": ^3.2.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ecbb77e6f38be40f37ea62a73f263474ff3ccf4c1a7f8ad806e5aa9c698cb2736bb82a65c5d5c0ff7d990b24b5bc457c175c45847e2d2730e53314650cec8864 + languageName: node + linkType: hard + +"@react-aria/searchfield@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/searchfield@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/textfield": ^3.7.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/searchfield": ^3.3.1 + "@react-types/button": ^3.6.1 + "@react-types/searchfield": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: aae0f23c6d2739b18009fc3167587a8a2b32e9304a41d9b21f5104b2c9d828f8b097e7b7ae647ee84416f5a4e5b2bc2c28efdeb0cd11f5e31b6b7704a5703ca8 + languageName: node + linkType: hard + +"@react-aria/select@npm:^3.8.1": + version: 3.8.1 + resolution: "@react-aria/select@npm:3.8.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/listbox": ^3.6.1 + "@react-aria/menu": ^3.6.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-aria/visually-hidden": ^3.4.1 + "@react-stately/select": ^3.3.1 + "@react-types/button": ^3.6.1 + "@react-types/select": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a05399b2564fefbd1a5bfd82ef9a5da2ed42f88c5a04a17361ad5a7030212e34629175efdb75e7c5c9a7b926d63ed9b0ad3369674853c38205113b85e8482154 + languageName: node + linkType: hard + +"@react-aria/selection@npm:^3.10.1": + version: 3.10.1 + resolution: "@react-aria/selection@npm:3.10.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/collections": ^3.4.3 + "@react-stately/selection": ^3.10.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 10fce36a292c7da796c10cf8f781b5a242528d846af76676ed7bc9468e66a92f7208d433636a9f95947ee845ee6f54df942fbbd66c06658b57f11619d76a57fd + languageName: node + linkType: hard + +"@react-aria/separator@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-aria/separator@npm:3.2.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 8d199b23786e96a4e4808a12600ec83b77cb59d643c0785638a5c3ee6d86f1f766d04bb598bc1827091efe12cc1a2d1ba4689a7869a2e2d7fa4b59e5f709e5c0 + languageName: node + linkType: hard + +"@react-aria/slider@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-aria/slider@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/radio": ^3.5.1 + "@react-stately/slider": ^3.2.1 + "@react-types/radio": ^3.2.3 + "@react-types/shared": ^3.14.1 + "@react-types/slider": ^3.2.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 49cb9561d790d4f34ba9d6d2dd5c39ede9400f63062c59926d6e604a5f852ef0c3979fabebb786a1573f8902f542fd4f5299c931580e08e97d32cfbd3864471d + languageName: node + linkType: hard + +"@react-aria/spinbutton@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-aria/spinbutton@npm:3.1.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/utils": ^3.13.3 + "@react-types/button": ^3.6.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a829c6b9047d104f23224368569354a2ab8fc8a415103fa1d571a4028f4e1954525a965bafdc82ae5d1082a6b0b5cf2623c1b93b6f053446733326214d980fd9 + languageName: node + linkType: hard + +"@react-aria/ssr@npm:^3.3.0": + version: 3.3.0 + resolution: "@react-aria/ssr@npm:3.3.0" + dependencies: + "@babel/runtime": ^7.6.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 0b7677ef521c65452460601dce3c264b67baa75ef7c99e9755ea55913765054156b6157c9c42e3d56aba86d1704b8b2aeb7672e4084f2f375fe1ec481e33c8c6 + languageName: node + linkType: hard + +"@react-aria/switch@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-aria/switch@npm:3.2.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/toggle": ^3.3.3 + "@react-stately/toggle": ^3.4.1 + "@react-types/switch": ^3.2.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f83f298e9e90710a95eacf166ec52d9e090bd9bda0199148f2084066a70fcd28915a754d32e4ec1c25017fae0dc7c8d84a51c58bf7b43e45a0a5eda8a5b95028 + languageName: node + linkType: hard + +"@react-aria/table@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/table@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/grid": ^3.4.1 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/live-announcer": ^3.1.1 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/table": ^3.4.0 + "@react-stately/virtualizer": ^3.3.0 + "@react-types/checkbox": ^3.3.3 + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + "@react-types/table": ^3.3.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 1a056f1d6d3e65a075ab1976bdd5c4aa4ec31a17b1dedab93cc566dd733e919fb758b7feb05e10f5994fcab0db3c71cd787bcda02c4cc1e18878782b1071de0c + languageName: node + linkType: hard + +"@react-aria/tabs@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/tabs@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/selection": ^3.10.1 + "@react-aria/utils": ^3.13.3 + "@react-stately/list": ^3.5.3 + "@react-stately/tabs": ^3.2.1 + "@react-types/shared": ^3.14.1 + "@react-types/tabs": ^3.1.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 71cb064187c48b619e68455c244e9d24b4c58bf7055d7a6bec0c8f72d18beca212b4aaab43656aa5d8c86a9c95028effda59476f0e26fae3c6b8158c64d57b32 + languageName: node + linkType: hard + +"@react-aria/textfield@npm:^3.7.1": + version: 3.7.1 + resolution: "@react-aria/textfield@npm:3.7.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/label": ^3.4.1 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + "@react-types/textfield": ^3.5.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 82d15558e1f7d1b61eeb4e0804079c3de595d75177d6873339402d94a220b3458eda3630f343ff7f4b124c0c9f6c9a72248fb79faccf354f3df55c0a50b177be + languageName: node + linkType: hard + +"@react-aria/toggle@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-aria/toggle@npm:3.3.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/toggle": ^3.4.1 + "@react-types/checkbox": ^3.3.3 + "@react-types/shared": ^3.14.1 + "@react-types/switch": ^3.2.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 03aef4d35d24aee76cfd9968715dd577cb0190b3aa3e74cc03666ea3b9388f133c95bff8cd8277bfe04d9f59703b8efa1d5c45dc17cbe3153b8475a553c73ac9 + languageName: node + linkType: hard + +"@react-aria/tooltip@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-aria/tooltip@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/focus": ^3.8.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/tooltip": ^3.2.1 + "@react-types/shared": ^3.14.1 + "@react-types/tooltip": ^3.2.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: e593b4940fc30bd3ba2d52c12f6e361e1bd1e277a64597982f826a2dcdd02b2b04383297117e4edb13f8078be81fb51a688704cebf67dd6f74fe0b71240abd35 + languageName: node + linkType: hard + +"@react-aria/utils@npm:^3.13.3": + version: 3.13.3 + resolution: "@react-aria/utils@npm:3.13.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/ssr": ^3.3.0 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + clsx: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b6d87ddb8e1d93b00405473099390c854647d81c0419de53cc4a7f02bdcca6d030776fba9f4b241400af13082bafc820dd5ce05c168e8f5a2c43a1b2660fb2ad + languageName: node + linkType: hard + +"@react-aria/visually-hidden@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-aria/visually-hidden@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/interactions": ^3.11.0 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + clsx: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: aea61d3ffbc2ac36074227cce1d41847ec250756a822874592d44830124da06b5e2f5b10b0726a38fe4263f19b5bc8fd7d7e141d2c4dc8853411913ff730fd8f + languageName: node + linkType: hard + +"@react-spectrum/dnd@npm:3.0.0-alpha.6": + version: 3.0.0-alpha.6 + resolution: "@react-spectrum/dnd@npm:3.0.0-alpha.6" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/dnd": 3.0.0-alpha.12 + "@react-stately/dnd": 3.0.0-alpha.10 + "@react-types/shared": ^3.14.1 + peerDependencies: + "@react-spectrum/provider": ^3.0.0 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 20aad8b50936966219e4ae645e2cfaadfd7ce5df0f1aec895a663ae7f7b73bf26a076166f6bbb2041764d7598a5ac3f8128b1fd37c3deb9b1f4dc1fbe02b7fbd + languageName: node + linkType: hard + +"@react-spring/animated@npm:~9.3.0": + version: 9.3.2 + resolution: "@react-spring/animated@npm:9.3.2" + dependencies: + "@react-spring/shared": ~9.3.0 + "@react-spring/types": ~9.3.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + checksum: e42fd174403a692d20f9cdc2511f5f4b1eed6282b4160d1eb0d8421ccce0524f4eaee968712f4e5f9b907a9f941ae3dd037ad35f13eacef8860fcfb90243a4fc + languageName: node + linkType: hard + +"@react-spring/core@npm:~9.3.0": + version: 9.3.2 + resolution: "@react-spring/core@npm:9.3.2" + dependencies: + "@react-spring/animated": ~9.3.0 + "@react-spring/shared": ~9.3.0 + "@react-spring/types": ~9.3.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + checksum: 1504143b9446d32c3877c8224c3fee3b6d18c0218b04b6766de7164ed645932808066ef12af63b06123a3da168f6bbc0fdf325c337b17d7f13d8a09f4a37f608 + languageName: node + linkType: hard + +"@react-spring/rafz@npm:~9.3.0": + version: 9.3.2 + resolution: "@react-spring/rafz@npm:9.3.2" + checksum: 8213d4940f23f93057dc2e7dc1e4aeca22622bfcfc9b42ab60d783e82f7ffe067e9537e7a397cfcb5634ea7e46d8ea55d06ffe4bd67a99f09ce06dde5e13957a + languageName: node + linkType: hard + +"@react-spring/shared@npm:~9.3.0": + version: 9.3.2 + resolution: "@react-spring/shared@npm:9.3.2" + dependencies: + "@react-spring/rafz": ~9.3.0 + "@react-spring/types": ~9.3.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + checksum: b04dc19fc6a8d585f38af74149e94fbf12b2a9ae9af6254abb02f105a7f77ce6a1e987ff7aaf4bb013786cd0959e41fca45709c4d57094abfca7b12db507a243 + languageName: node + linkType: hard + +"@react-spring/types@npm:~9.3.0": + version: 9.3.2 + resolution: "@react-spring/types@npm:9.3.2" + checksum: 29fb2be7cd44c3de534d6b0d914b1c51fdc6ea354098727e605218b0e733c3c0ab01f54670f1b8b9b1949d984e399ce3ec153640ae9c590aee9622f33900d16b + languageName: node + linkType: hard + +"@react-spring/web@npm:9.3.1": + version: 9.3.1 + resolution: "@react-spring/web@npm:9.3.1" + dependencies: + "@react-spring/animated": ~9.3.0 + "@react-spring/core": ~9.3.0 + "@react-spring/shared": ~9.3.0 + "@react-spring/types": ~9.3.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + checksum: 223ce3f849df23a7c4f630ea1e5be91a12257e35e2c8c10b939568302176d3881c1037d50daca4238a6383ddaa477cdc4971163a9da181b790abe91cae4b8c50 + languageName: node + linkType: hard + +"@react-stately/calendar@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-stately/calendar@npm:3.0.2" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/date": ^3.0.1 + "@react-stately/utils": ^3.5.1 + "@react-types/calendar": ^3.0.2 + "@react-types/datepicker": ^3.1.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: c093cab8761b1e16603abcde63f78dfefdb7fdf4cc269e41602ab3a7c93f9391d29ac68cc66e030c553305af7d96ff9afa3795123211a59316819937a8181956 + languageName: node + linkType: hard + +"@react-stately/checkbox@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/checkbox@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/toggle": ^3.4.1 + "@react-stately/utils": ^3.5.1 + "@react-types/checkbox": ^3.3.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 9035b595fa21cc1bef7e04249ec9df2293e93310dd644e4d32087ce19bd77aae38db3e676f6fdffbde875bc9a318f05dd60c61ab6e0d9b524222438e7ef31cd7 + languageName: node + linkType: hard + +"@react-stately/collections@npm:^3.4.3": + version: 3.4.3 + resolution: "@react-stately/collections@npm:3.4.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f9045cdac0b20f7d7464ac37c0402511f7c5a727676d0cfefef74a553247d0dd1c816ea5804aac318d85ea5708599f9c9c2e8bd37165b5c6eec100e27f3832b9 + languageName: node + linkType: hard + +"@react-stately/combobox@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/combobox@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/list": ^3.5.3 + "@react-stately/menu": ^3.4.1 + "@react-stately/select": ^3.3.1 + "@react-stately/utils": ^3.5.1 + "@react-types/combobox": ^3.5.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 3e9a9050e8e20c96ae703876e652d28d2e3cf9dca79008d8e0f9fd096e88f74215add97e7d4aec9fe93afd64ebd676e5593d5178a28ad76c180207740fc47712 + languageName: node + linkType: hard + +"@react-stately/datepicker@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-stately/datepicker@npm:3.0.2" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/date": ^3.0.1 + "@internationalized/string": ^3.0.0 + "@react-stately/overlays": ^3.4.1 + "@react-stately/utils": ^3.5.1 + "@react-types/datepicker": ^3.1.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: d0250033d8f4625442177eac1ced6fe446877df9607bd1d7bdea11daae47166072304ee66d4ce1fe12886ef24c0cc1983ac5807a1fe07b05a5749d6b8302f47b + languageName: node + linkType: hard + +"@react-stately/dnd@npm:3.0.0-alpha.10": + version: 3.0.0-alpha.10 + resolution: "@react-stately/dnd@npm:3.0.0-alpha.10" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/selection": ^3.10.3 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f0138d525660f6d5e670a2bc26a63c5a12f91247f4807c3e53c903b737bdddea13e7532661a6b042797f460183df0bb95f78c2162d42af82944bc9688f0acca5 + languageName: node + linkType: hard + +"@react-stately/grid@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-stately/grid@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/selection": ^3.10.3 + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 84e1f24d2dcac51b1ab99f0ad403c965eb9988fa236054a5c137efb1917a455d56a1b78f820a77c3af38895d60a24884cfeac5a482b36390b629612ee8c7e7f3 + languageName: node + linkType: hard + +"@react-stately/layout@npm:^3.7.0": + version: 3.7.0 + resolution: "@react-stately/layout@npm:3.7.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/virtualizer": ^3.3.0 + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + "@react-types/table": ^3.3.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: d17510cd448a18b22f7d5c4e7ddf2bab168c6836a7c674d2ddef9f8b352f3a42b09bf60bbbdbe0b8e9a291206709a31447b7db5535aab74663c0f0af272c394f + languageName: node + linkType: hard + +"@react-stately/list@npm:^3.5.3": + version: 3.5.3 + resolution: "@react-stately/list@npm:3.5.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/collections": ^3.4.3 + "@react-stately/selection": ^3.10.3 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 162ba719db06a1649bbeb655c78e8a3f3c17a4c02f3318479ce2cc71940052f4a3cc98e67fd604f48ed89f199c731fb6d7c4d6e7b36d53593a0fc9b38d5e465c + languageName: node + linkType: hard + +"@react-stately/menu@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-stately/menu@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/overlays": ^3.4.1 + "@react-stately/utils": ^3.5.1 + "@react-types/menu": ^3.7.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a944d6e3a3caf400ffc52738ee8d586db6c6846d0ecc009de4bbedc88202f63d6bbddbd3d577f730f98f28404b077676af4c307f4ba09314c79cf56087a5aa8c + languageName: node + linkType: hard + +"@react-stately/numberfield@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/numberfield@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@internationalized/number": ^3.1.1 + "@react-stately/utils": ^3.5.1 + "@react-types/numberfield": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 5698d237c8fbe65cc7ab85c586ffadd92d085f15cab542003419deeccc2f13f2aa839dc844df8853648d892d6580fd4dd15a0b0d4eba86a467afbdb8d3c1675f + languageName: node + linkType: hard + +"@react-stately/overlays@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-stately/overlays@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/utils": ^3.5.1 + "@react-types/overlays": ^3.6.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 3e0e8711c55198b75cb23a682530969c997fdd21c280a9a1356327ff3806252a70ef13e4efc7734902edfd58d6c2cc9d2624a37d8394ad44e9d33b09186510e3 + languageName: node + linkType: hard + +"@react-stately/radio@npm:^3.5.1": + version: 3.5.1 + resolution: "@react-stately/radio@npm:3.5.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/utils": ^3.5.1 + "@react-types/radio": ^3.2.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 7a60de8afb5d8ccaf33da66613ae55a4b2eca75bacae902574282c33ab66684b1ae5db95b2743fdcc926d1c0464af7e6d837f6a5b85bb00836a9c78ba65c3623 + languageName: node + linkType: hard + +"@react-stately/searchfield@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-stately/searchfield@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/utils": ^3.5.1 + "@react-types/searchfield": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f52776a294450382ea9f2abacea6b2972ea4f96ef6ffaff33c62f783881ceb74cd6aec959178499d6c7acf49b3a671d1902f0eb9cc4f2dba486ca88f7514693b + languageName: node + linkType: hard + +"@react-stately/select@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-stately/select@npm:3.3.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/collections": ^3.4.3 + "@react-stately/list": ^3.5.3 + "@react-stately/menu": ^3.4.1 + "@react-stately/selection": ^3.10.3 + "@react-stately/utils": ^3.5.1 + "@react-types/select": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 0701cadd640fdea8a3a1c7048e459f701fc8ec9c0ef1fb9692fd70faa5bb7ce23475aba988f57dff90a3db71cbbf8b1ba49edc3df43e744550fbd0e2dcc3575f + languageName: node + linkType: hard + +"@react-stately/selection@npm:^3.10.3": + version: 3.10.3 + resolution: "@react-stately/selection@npm:3.10.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/collections": ^3.4.3 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f65af198fa9199bc6bcf76279e2131b605e3ce449cc61d404de34993c81f499d0aba34916e8e8fd867d01ae60786ea3c3b725f3c73153674812bf29e64c6a531 + languageName: node + linkType: hard + +"@react-stately/slider@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/slider@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/i18n": ^3.6.0 + "@react-aria/utils": ^3.13.3 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + "@react-types/slider": ^3.2.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 3d20eae41b79e481fc45cb4671b17ea20010f199790c963766a58067df18c1b83b41b1394ff3b053b32306cd952bad12331dec09c2a6a6c0c060f336aafee0ca + languageName: node + linkType: hard + +"@react-stately/table@npm:^3.4.0": + version: 3.4.0 + resolution: "@react-stately/table@npm:3.4.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/collections": ^3.4.3 + "@react-stately/grid": ^3.3.1 + "@react-stately/selection": ^3.10.3 + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + "@react-types/table": ^3.3.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f3571875fe9978d1f99554d8a31b3af3ced6ac84fde77ed175620f5ce76952833b98ee41b383f5098489c41f504942cedcffe604a4ec6158bbe320267eb70d01 + languageName: node + linkType: hard + +"@react-stately/tabs@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/tabs@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/list": ^3.5.3 + "@react-stately/utils": ^3.5.1 + "@react-types/tabs": ^3.1.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 593d4ea004ed89156ebf6e2eea401d30e4b06e9eae0f83752550bc5d3d776008577ba0e6baca9791c2e1c0af0f15881a8f95f18923132af08de172cecf097d20 + languageName: node + linkType: hard + +"@react-stately/toggle@npm:^3.4.1": + version: 3.4.1 + resolution: "@react-stately/toggle@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/utils": ^3.5.1 + "@react-types/checkbox": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 6cc297ac5c840aa20a6d304947a4d869b857c9dc522b7e77cf798f1815ebd5e5ae1f00aeb812fa452fbbfada1069e814b9e1aaf2751b747f875f8b88d88c21fe + languageName: node + linkType: hard + +"@react-stately/tooltip@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/tooltip@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/overlays": ^3.4.1 + "@react-stately/utils": ^3.5.1 + "@react-types/tooltip": ^3.2.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: dbb650986c11284dc45b6c0940e3a5aecb7d5e1af92828ae93b4ec1441b580461340033f427523b16f216afb815ebdc491f7aae361e5cd3bcc3dcea1268c76ab + languageName: node + linkType: hard + +"@react-stately/tree@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-stately/tree@npm:3.3.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-stately/collections": ^3.4.3 + "@react-stately/selection": ^3.10.3 + "@react-stately/utils": ^3.5.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 4e1a94cb478124a2443e84dbf0160dd3a5298e79478336f07003b8c5fcdb26043c65a94439a17315cf00e7f66bf6fd5e3e6fbcb44bced3352554d8f7be94899a + languageName: node + linkType: hard + +"@react-stately/utils@npm:^3.5.1": + version: 3.5.1 + resolution: "@react-stately/utils@npm:3.5.1" + dependencies: + "@babel/runtime": ^7.6.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f748331ae393f97b3e6fcccd37b767358f49229520b9500f82ed4c620bff36ef3c01d4ba9679ac7b9d6d78c5f6e711186c98bd0e6482ec27a6fbf26c5d0aa3cc + languageName: node + linkType: hard + +"@react-stately/virtualizer@npm:^3.3.0": + version: 3.3.0 + resolution: "@react-stately/virtualizer@npm:3.3.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/utils": ^3.13.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b7eb545697756ae404c03b3c6cfe0bcc7d2ece1d1ef13ecdd1eb8a4ddf27987875fcd548e314d1320bf79e448f5bbe2a1b04439505450c2a7c9e96f5921d7517 + languageName: node + linkType: hard + +"@react-types/breadcrumbs@npm:^3.4.3": + version: 3.4.3 + resolution: "@react-types/breadcrumbs@npm:3.4.3" + dependencies: + "@react-types/link": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 617f7d163062c627e0c58510a47472b0af30a42bb2e1c083f6690f9808e340a2062c643b5292a8625ac84a7522f5870565f6be58c363ccc8f91f7c82f42f8dde + languageName: node + linkType: hard + +"@react-types/button@npm:^3.6.1": + version: 3.6.1 + resolution: "@react-types/button@npm:3.6.1" + dependencies: + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: c9a177a436be81fe26cc3a876e73b708b235a3c713318b9956cabf942476996cb11e59fe614300647a3d96089873c9d7036dda24e83bf7e5a4c2aa836726f0dc + languageName: node + linkType: hard + +"@react-types/calendar@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-types/calendar@npm:3.0.2" + dependencies: + "@internationalized/date": ^3.0.1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a3fd271d85064837c3b7a495e4048c25da1bbbc21015cfadd970f9959e8c802c9152e25ea772ffd815655e392ce07ce75c688726e70bb4cf6959605bc8257c8e + languageName: node + linkType: hard + +"@react-types/checkbox@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-types/checkbox@npm:3.3.3" + dependencies: + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: d1da491ff3bf14f894dbeab5ace3a397ead306d2cc4a820d2a653e038a5628495417feb10a4e07c05dcfce208ae9303c35de7e57d1b21a6b59ca1acca11b80d8 + languageName: node + linkType: hard + +"@react-types/combobox@npm:^3.5.3": + version: 3.5.3 + resolution: "@react-types/combobox@npm:3.5.3" + dependencies: + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 41e1371f1efa48fe4d56afffeca59d1ed9dad75565c3d67fdf9f6c594529113ce9a8053b95d419682364878a6df0fd6a7178c20e6735778eea2abe74de1ca24f languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" +"@react-types/datepicker@npm:^3.1.1": + version: 3.1.1 + resolution: "@react-types/datepicker@npm:3.1.1" dependencies: - "@nodelib/fs.stat": 2.0.5 - run-parallel: ^1.1.9 - checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + "@internationalized/date": ^3.0.1 + "@react-types/overlays": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: a3ab8ae22da8105ffebdebb5c89f212cda4d6f2203f7579cbd733e36afb4d2c7e4f986082becacaa66e7d1b4a99ef109952ddae7760d4bb0685e71d53894e316 languageName: node linkType: hard -"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 +"@react-types/dialog@npm:^3.4.3": + version: 3.4.3 + resolution: "@react-types/dialog@npm:3.4.3" + dependencies: + "@react-types/overlays": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 96946e273cfb81d0d536e6f4077e8a1ca19a5a9cb3ae2e26e0993f15650726584574dffc1af1374f4ab16e8940d82e275d44f8d54719da72b6584ff85867197b languageName: node linkType: hard -"@nodelib/fs.stat@npm:^1.1.2": - version: 1.1.3 - resolution: "@nodelib/fs.stat@npm:1.1.3" - checksum: 318deab369b518a34778cdaa0054dd28a4381c0c78e40bbd20252f67d084b1d7bf9295fea4423de2c19ac8e1a34f120add9125f481b2a710f7068bcac7e3e305 +"@react-types/grid@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-types/grid@npm:3.1.3" + dependencies: + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 124b366436160ac7b88368a8be37abf4c703bde3fc1275e720f76d9ee8d0a10825fc5dd314b5eb6bb17b7d0c87091608d9b96d9521329ee5baeb94ab08fa3835 languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" +"@react-types/label@npm:^3.6.3": + version: 3.6.3 + resolution: "@react-types/label@npm:3.6.3" dependencies: - "@nodelib/fs.scandir": 2.1.5 - fastq: ^1.6.0 - checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 25f722b15c1a823f61f5a3200268c3973ab1888d7434621a12e64eca9065427a736a2334f4c2108f590a6b85fc512dda99d21d271f71634efbe5dd3ebb01229d languageName: node linkType: hard -"@npmcli/fs@npm:^1.0.0": - version: 1.1.1 - resolution: "@npmcli/fs@npm:1.1.1" +"@react-types/link@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-types/link@npm:3.3.3" dependencies: - "@gar/promisify": ^1.0.1 - semver: ^7.3.5 - checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965 + "@react-aria/interactions": ^3.11.0 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: d969b88631376b3d66acb0677b1c9efc6024c2484f9a28269b3855bf4a2f2bd72b0ddc1c3b1de2f4f2a86631b3135df60997af3923f8079ccfc5be59c19466ba languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" +"@react-types/list@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-types/list@npm:3.0.0" dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b + "@react-spectrum/dnd": 3.0.0-alpha.6 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: bd14b001c559e9d35f113a41e3e2fc5051c271d1b7e3a751521578a40a946ed83efdda37d8bfd6246b190c702e0e1ee69e3b4c079cd40fc9053510dbac2f396f languageName: node linkType: hard -"@npmcli/move-file@npm:^1.0.1, @npmcli/move-file@npm:^1.1.2": - version: 1.1.2 - resolution: "@npmcli/move-file@npm:1.1.2" +"@react-types/listbox@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-types/listbox@npm:3.3.3" dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: e07c9f4b939add09ad13cfabe20ed35e9508f6401c332ed2f02a706d4a4b92bff46bb07084c5c90da0e39bf5188750f2d72e8e08ce9c64fb9680231b09279971 languageName: node linkType: hard -"@opencensus/core@npm:0.0.9": - version: 0.0.9 - resolution: "@opencensus/core@npm:0.0.9" +"@react-types/menu@npm:^3.7.1": + version: 3.7.1 + resolution: "@react-types/menu@npm:3.7.1" dependencies: - continuation-local-storage: ^3.2.1 - log-driver: ^1.2.7 - semver: ^5.5.0 - shimmer: ^1.2.0 - uuid: ^3.2.1 - checksum: 37af50394cbf759a835182eedf9fbf328232916bdd2174e75221cf9f29393621d0d05eaf24bb61b89b50c740bc4f625e5c8f891e2db627ce6dbf1888d98857e5 + "@react-types/overlays": ^3.6.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 349443d1bd23bf64a9af57bef57d8ebfebfc6e82dbcef5cfd8ba778afc998f8dc3cebaae80728e6017b0d12b9e5aaea783254df36dd1b82a048b9e3c0e095795 languageName: node linkType: hard -"@opencensus/core@npm:^0.0.8": - version: 0.0.8 - resolution: "@opencensus/core@npm:0.0.8" +"@react-types/meter@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-types/meter@npm:3.2.3" dependencies: - continuation-local-storage: ^3.2.1 - log-driver: ^1.2.7 - semver: ^5.5.0 - shimmer: ^1.2.0 - uuid: ^3.2.1 - checksum: b4ad6c9a4963407195ab9c2f2751888b1462807c547bd9a7ceb561c473073aacc17555afa9e37cbda01805be5e954085284629e502ed5d3fe7b3c628d9ec2882 + "@react-types/progress": ^3.2.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: c7be88710e0a14e94a3cad9453296a17aadb2fefd63ee96bb1cbe382dbbf8482bd171fec33b0bcedfa8bbac396186ad1820a41fff2b20c912d20d8a2ced95edb languageName: node linkType: hard -"@opencensus/propagation-b3@npm:0.0.8": - version: 0.0.8 - resolution: "@opencensus/propagation-b3@npm:0.0.8" +"@react-types/numberfield@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-types/numberfield@npm:3.3.3" dependencies: - "@opencensus/core": ^0.0.8 - uuid: ^3.2.1 - checksum: c480bd181062ac15f23d5c833a1b1760f2734e013ab7d0bc3fc24d7d1f8330f38f426a56df6d1baf1384727b3dbc1f9ba2851caaac2125500327117fbafa8f70 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: b0f6627157dea0ce8a8fa3434c55bbbc69c4b46c84024d6da6f97091eae45c8b4f9b5b842c2ff82712c1b2e407b4acbd0d476ceda25abd3b9402a0b4573b3b52 languageName: node linkType: hard -"@playwright/test@npm:^1.22.2": - version: 1.23.1 - resolution: "@playwright/test@npm:1.23.1" +"@react-types/overlays@npm:^3.6.3": + version: 3.6.3 + resolution: "@react-types/overlays@npm:3.6.3" dependencies: - "@types/node": "*" - playwright-core: 1.23.1 - bin: - playwright: cli.js - checksum: 27fc6b4424dfb0f0dfaaecb01a33bf77c0f95e6a1e635fe85dd4ad36e5053afdbe4ad1de4be1c5705e482451b43f7ddacf667f6c5b70ab5f8e32c30110211c29 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 8688db82adeda13e922f9805a5c9bd9f64e97e91c0ebf32409964e9d661828a4bb31907551dcdcd611807efa9824ff78aa8cb2ee4b0acfab001cbff5572336d4 languageName: node linkType: hard -"@pm2/agent@npm:~2.0.0": - version: 2.0.1 - resolution: "@pm2/agent@npm:2.0.1" +"@react-types/progress@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-types/progress@npm:3.2.3" dependencies: - async: ~3.2.0 - chalk: ~3.0.0 - dayjs: ~1.8.24 - debug: ~4.3.1 - eventemitter2: ~5.0.1 - fast-json-patch: ^3.0.0-1 - fclone: ~1.0.11 - nssocket: 0.6.0 - pm2-axon: ~4.0.1 - pm2-axon-rpc: ~0.7.0 - proxy-agent: ~5.0.0 - semver: ~7.2.0 - ws: ~7.4.0 - checksum: d15e1ccec8e25bd020eabf2a59e0d2896082414b7950e918a1a19485ef1001012d7df6063fb720f453d9a2cd887abc80d576f985d2c28c127a7c1a16b95aee65 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 16cb619cbc0bc9abdb42479340493adf87842de0ae325932dae5b226f0c022321ed1e6e60ec6935bc27b638c99266786e090ec9f34f9a2b7b90c0fb25326b82a languageName: node linkType: hard -"@pm2/io@npm:~5.0.0": - version: 5.0.0 - resolution: "@pm2/io@npm:5.0.0" +"@react-types/radio@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-types/radio@npm:3.2.3" dependencies: - "@opencensus/core": 0.0.9 - "@opencensus/propagation-b3": 0.0.8 - async: ~2.6.1 - debug: ~4.3.1 - eventemitter2: ^6.3.1 - require-in-the-middle: ^5.0.0 - semver: 6.3.0 - shimmer: ^1.2.0 - signal-exit: ^3.0.3 - tslib: 1.9.3 - checksum: f912096e823003de941a539d1098243cdc39b896704dea176e75a1d4fb0e0573b552c0a30e5198c2415fee9e1d8365ce0a8e78257eb85a8bfb994f1fb8153af1 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: ce37d92a7e6665a9900b232aae68978bf1c82b4dffd30cc896c6382df7a9bb8a501a30f1b819d0630f9cbf21af3cb6f51de05fbaeaef4a1f250e5d39276eba59 languageName: node linkType: hard -"@pm2/js-api@npm:~0.6.7": - version: 0.6.7 - resolution: "@pm2/js-api@npm:0.6.7" +"@react-types/searchfield@npm:^3.3.3": + version: 3.3.3 + resolution: "@react-types/searchfield@npm:3.3.3" dependencies: - async: ^2.6.3 - axios: ^0.21.0 - debug: ~4.3.1 - eventemitter2: ^6.3.1 - ws: ^7.0.0 - checksum: 43d1e4d06328ad18d8929225a0732fa20701694dea0e3af0ea692239e0adc45ef2f48a794599a8df88bc705d2e428b5b8018deb3709983c91ed0b74a834b2f21 + "@react-types/shared": ^3.14.1 + "@react-types/textfield": ^3.5.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: cee59f6ad1da98cc01b81252ef91ebddf0a46df73e4cb3016474c9ad288a0d7b3de2d4607285de97ec23ecaba50391980268ac69b2def096c1fe3a33ecffc686 languageName: node linkType: hard -"@pm2/pm2-version-check@npm:latest": - version: 1.0.4 - resolution: "@pm2/pm2-version-check@npm:1.0.4" +"@react-types/select@npm:^3.6.3": + version: 3.6.3 + resolution: "@react-types/select@npm:3.6.3" dependencies: - debug: ^4.3.1 - checksum: 663438d9154db3c11bc7d41a4838162542db9cb4cf989fe8936e9bd9e5b99e3a58d73c8888afa1180a8cb93375c1d165bb397939de8a5ab8507d13d2aed2a086 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 472d3086e13ca18857659c5a93e36d5e00c4f1077fd627b16ed93641e6ec39aed77a6cb819e3115616486df3178c2e725aef8dd95cd36fc297819b78111d10b8 languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": - version: 0.5.7 - resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7" - dependencies: - ansi-html-community: ^0.0.8 - common-path-prefix: ^3.0.0 - core-js-pure: ^3.8.1 - error-stack-parser: ^2.0.6 - find-up: ^5.0.0 - html-entities: ^2.1.0 - loader-utils: ^2.0.0 - schema-utils: ^3.0.0 - source-map: ^0.7.3 +"@react-types/shared@npm:^3.14.1": + version: 3.14.1 + resolution: "@react-types/shared@npm:3.14.1" peerDependencies: - "@types/webpack": 4.x || 5.x - react-refresh: ">=0.10.0 <1.0.0" - sockjs-client: ^1.4.0 - type-fest: ">=0.17.0 <3.0.0" - webpack: ">=4.43.0 <6.0.0" - webpack-dev-server: 3.x || 4.x - webpack-hot-middleware: 2.x - webpack-plugin-serve: 0.x || 1.x - peerDependenciesMeta: - "@types/webpack": - optional: true - sockjs-client: - optional: true - type-fest: - optional: true - webpack-dev-server: - optional: true - webpack-hot-middleware: - optional: true - webpack-plugin-serve: - optional: true - checksum: 3490649181878cc8808fb91f3870ef095e5a1fb9647b3ac83740df07379c9d1cf540f24bf2b09d5f26a3a8c805b2c6b9c5be7192bdb9317d0ffffa67426e9f66 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 117fe230f5a26b7fcaf535c1cfb7c4d42416b0f49d0e0b3436fef2a5851234967908c4e884fc5f2a99a04bee2543543348346a04e1f3f45aaa14c42b6f08491a languageName: node linkType: hard -"@react-spring/animated@npm:~9.3.0": - version: 9.3.2 - resolution: "@react-spring/animated@npm:9.3.2" +"@react-types/slider@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-types/slider@npm:3.2.1" dependencies: - "@react-spring/shared": ~9.3.0 - "@react-spring/types": ~9.3.0 + "@react-types/shared": ^3.14.1 peerDependencies: - react: ^16.8.0 || ^17.0.0 - checksum: e42fd174403a692d20f9cdc2511f5f4b1eed6282b4160d1eb0d8421ccce0524f4eaee968712f4e5f9b907a9f941ae3dd037ad35f13eacef8860fcfb90243a4fc + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 3c64ab2d99fd14debd74181ab4faef43656b274f00443899564e89f3b4b8d9c327184a9c236e4f69c4efc8cba0eca0a0aeae686dcf9a521a7749bab4e0bbdfbb languageName: node linkType: hard -"@react-spring/core@npm:~9.3.0": - version: 9.3.2 - resolution: "@react-spring/core@npm:9.3.2" +"@react-types/switch@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-types/switch@npm:3.2.3" dependencies: - "@react-spring/animated": ~9.3.0 - "@react-spring/shared": ~9.3.0 - "@react-spring/types": ~9.3.0 + "@react-types/checkbox": ^3.3.3 + "@react-types/shared": ^3.14.1 peerDependencies: - react: ^16.8.0 || ^17.0.0 - checksum: 1504143b9446d32c3877c8224c3fee3b6d18c0218b04b6766de7164ed645932808066ef12af63b06123a3da168f6bbc0fdf325c337b17d7f13d8a09f4a37f608 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 07346d06f39606580e95b5fcb96cdf91731e33697faa183d1f5290d0ca82117b4de80ea7ce009066da9aa276ea3cdcd0d11bfca652eb8eee1471101754ed7342 languageName: node linkType: hard -"@react-spring/rafz@npm:~9.3.0": - version: 9.3.2 - resolution: "@react-spring/rafz@npm:9.3.2" - checksum: 8213d4940f23f93057dc2e7dc1e4aeca22622bfcfc9b42ab60d783e82f7ffe067e9537e7a397cfcb5634ea7e46d8ea55d06ffe4bd67a99f09ce06dde5e13957a +"@react-types/table@npm:^3.3.1": + version: 3.3.1 + resolution: "@react-types/table@npm:3.3.1" + dependencies: + "@react-types/grid": ^3.1.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 1d3e4f8bac6e886944f67c159224893e63ec500f18aaadc74613d9053382c53fd282a7ee9dc21616b7fc0e1291d6ec7ccae87ebca2abdd19e8b371fb8cb46abc languageName: node linkType: hard -"@react-spring/shared@npm:~9.3.0": - version: 9.3.2 - resolution: "@react-spring/shared@npm:9.3.2" +"@react-types/tabs@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-types/tabs@npm:3.1.3" dependencies: - "@react-spring/rafz": ~9.3.0 - "@react-spring/types": ~9.3.0 + "@react-types/shared": ^3.14.1 peerDependencies: - react: ^16.8.0 || ^17.0.0 - checksum: b04dc19fc6a8d585f38af74149e94fbf12b2a9ae9af6254abb02f105a7f77ce6a1e987ff7aaf4bb013786cd0959e41fca45709c4d57094abfca7b12db507a243 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 04a95bfb92d2fe44900135bbdd1d256622c51fc90ebecc3374d29eb69bbb77ec13156d39b9fe1806e66b726cf5bbe9ff64822e04e5f3bcacd2429e27c3c260e1 languageName: node linkType: hard -"@react-spring/types@npm:~9.3.0": - version: 9.3.2 - resolution: "@react-spring/types@npm:9.3.2" - checksum: 29fb2be7cd44c3de534d6b0d914b1c51fdc6ea354098727e605218b0e733c3c0ab01f54670f1b8b9b1949d984e399ce3ec153640ae9c590aee9622f33900d16b +"@react-types/textfield@npm:^3.5.3": + version: 3.5.3 + resolution: "@react-types/textfield@npm:3.5.3" + dependencies: + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: f684821edba64e0b525606590800bf2cb6aea98c7304956ed3b2bbcb129ba7734897a9ca1bd056c2f23bf515399fed654071de6a2037942093c2af1c07fad1a9 languageName: node linkType: hard -"@react-spring/web@npm:9.3.1": - version: 9.3.1 - resolution: "@react-spring/web@npm:9.3.1" +"@react-types/tooltip@npm:^3.2.3": + version: 3.2.3 + resolution: "@react-types/tooltip@npm:3.2.3" dependencies: - "@react-spring/animated": ~9.3.0 - "@react-spring/core": ~9.3.0 - "@react-spring/shared": ~9.3.0 - "@react-spring/types": ~9.3.0 + "@react-types/overlays": ^3.6.3 + "@react-types/shared": ^3.14.1 peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - checksum: 223ce3f849df23a7c4f630ea1e5be91a12257e35e2c8c10b939568302176d3881c1037d50daca4238a6383ddaa477cdc4971163a9da181b790abe91cae4b8c50 + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 5079ee2e561c2b9a7cc6e9dd22d48a26b0d61d79114aff730b7fc8348e199ec1db9e2ef96725f1682ebd5e93bbb223b920aa507f0f9997d8bc3df76f315077a6 languageName: node linkType: hard @@ -4123,6 +5586,7 @@ __metadata: queue-fifo: ^0.2.6 rc-scrollbars: ^1.1.5 react: ~17.0.2 + react-aria: ^3.19.0 react-dom: ~17.0.2 react-error-boundary: ^3.1.4 react-hook-form: ^7.30.0 @@ -11096,6 +12560,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.1.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 + languageName: node + linkType: hard + "cluster-key-slot@npm:1.1.0": version: 1.1.0 resolution: "cluster-key-slot@npm:1.1.0" @@ -18015,6 +19486,18 @@ __metadata: languageName: node linkType: hard +"intl-messageformat@npm:^10.1.0": + version: 10.1.4 + resolution: "intl-messageformat@npm:10.1.4" + dependencies: + "@formatjs/ecma402-abstract": 1.12.0 + "@formatjs/fast-memoize": 1.2.6 + "@formatjs/icu-messageformat-parser": 2.1.7 + tslib: 2.4.0 + checksum: 09c2cba0d64b9b9c99b9630b3f32661dd25886461eea5e8b6e0dac6b13b8ab0eb8bf2646bc73baa8b47501544f6cdb255d888617e22d056cce686849e05e2699 + languageName: node + linkType: hard + "into-stream@npm:^3.1.0": version: 3.1.0 resolution: "into-stream@npm:3.1.0" @@ -26353,6 +27836,49 @@ __metadata: languageName: node linkType: hard +"react-aria@npm:^3.19.0": + version: 3.19.0 + resolution: "react-aria@npm:3.19.0" + dependencies: + "@react-aria/breadcrumbs": ^3.3.1 + "@react-aria/button": ^3.6.1 + "@react-aria/calendar": ^3.0.2 + "@react-aria/checkbox": ^3.5.1 + "@react-aria/combobox": ^3.4.1 + "@react-aria/datepicker": ^3.1.1 + "@react-aria/dialog": ^3.3.1 + "@react-aria/focus": ^3.8.0 + "@react-aria/gridlist": ^3.0.0 + "@react-aria/i18n": ^3.6.0 + "@react-aria/interactions": ^3.11.0 + "@react-aria/label": ^3.4.1 + "@react-aria/link": ^3.3.3 + "@react-aria/listbox": ^3.6.1 + "@react-aria/menu": ^3.6.1 + "@react-aria/meter": ^3.3.1 + "@react-aria/numberfield": ^3.3.1 + "@react-aria/overlays": ^3.10.1 + "@react-aria/progress": ^3.3.1 + "@react-aria/radio": ^3.3.1 + "@react-aria/searchfield": ^3.4.1 + "@react-aria/select": ^3.8.1 + "@react-aria/separator": ^3.2.3 + "@react-aria/slider": ^3.2.1 + "@react-aria/ssr": ^3.3.0 + "@react-aria/switch": ^3.2.3 + "@react-aria/table": ^3.4.1 + "@react-aria/tabs": ^3.3.1 + "@react-aria/textfield": ^3.7.1 + "@react-aria/tooltip": ^3.3.1 + "@react-aria/utils": ^3.13.3 + "@react-aria/visually-hidden": ^3.4.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 67213326b319b2ccb1d50ed6a5e57f8f4893ae0c1ebfa2ec11d1f467173437ba88830bb596c17f0b5ac7fa7b385d686434ab859f69b8ff504a9054a8022c8175 + languageName: node + linkType: hard + "react-colorful@npm:^5.1.2": version: 5.5.1 resolution: "react-colorful@npm:5.5.1" @@ -30591,6 +32117,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.4.0, tslib@npm:^2.1.0": + version: 2.4.0 + resolution: "tslib@npm:2.4.0" + checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 + languageName: node + linkType: hard + "tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -30605,13 +32138,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0": - version: 2.4.0 - resolution: "tslib@npm:2.4.0" - checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" -- GitLab From 19be3b1c56db1758f5c2a04e8541a46cdcb7ac75 Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Fri, 9 Sep 2022 10:25:00 -0600 Subject: [PATCH 018/107] Chore: Cleanup endpoint handlers (#26749) Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> --- apps/meteor/app/api/server/lib/emailInbox.ts | 21 +- apps/meteor/app/api/server/v1/email-inbox.ts | 34 +- apps/meteor/app/api/server/v1/voip/events.ts | 2 +- .../app/api/server/v1/voip/omnichannel.ts | 20 +- apps/meteor/app/api/server/v1/voip/queues.ts | 6 +- .../app/livechat/imports/server/rest/agent.js | 14 +- .../imports/server/rest/dashboards.js | 18 +- .../imports/server/rest/departments.ts | 70 ++-- .../livechat/imports/server/rest/inquiries.js | 26 +- .../app/livechat/imports/server/rest/users.js | 6 +- .../app/livechat/server/api/v1/agent.js | 4 +- .../app/livechat/server/api/v1/message.js | 102 +++--- .../livechat/server/api/v1/offlineMessage.js | 30 +- .../app/livechat/server/api/v1/pageVisited.js | 2 +- .../meteor/app/livechat/server/api/v1/room.js | 113 +++--- .../app/livechat/server/api/v1/videoCall.js | 139 ++++--- .../app/livechat/server/methods/transfer.js | 2 + .../api-enterprise/server/canned-responses.js | 54 ++- .../server/lib/canned-responses.js | 2 +- .../livechat-enterprise/server/api/agents.js | 22 +- .../server/api/departments.js | 57 +-- .../server/api/inquiries.ts | 2 +- .../server/api/lib/inquiries.js | 4 - .../server/api/lib/monitors.js | 12 +- .../server/api/lib/priorities.ts | 19 +- .../server/api/lib/tags.ts | 12 +- .../server/api/lib/units.ts | 17 +- .../server/api/monitors.ts | 12 +- .../server/api/priorities.ts | 6 +- .../livechat-enterprise/server/api/rooms.ts | 6 +- .../livechat-enterprise/server/api/tags.ts | 4 +- .../livechat-enterprise/server/api/units.ts | 11 +- apps/meteor/tests/data/livechat/inboxes.ts | 32 ++ .../tests/end-to-end/api/livechat/00-rooms.ts | 101 ++++- .../end-to-end/api/livechat/11-email-inbox.ts | 346 ++++++++++++++++-- .../end-to-end/api/livechat/11-livechat.ts | 40 ++ 36 files changed, 839 insertions(+), 529 deletions(-) create mode 100644 apps/meteor/tests/data/livechat/inboxes.ts diff --git a/apps/meteor/app/api/server/lib/emailInbox.ts b/apps/meteor/app/api/server/lib/emailInbox.ts index 1e6e1db3619..dcfd147b6a1 100644 --- a/apps/meteor/app/api/server/lib/emailInbox.ts +++ b/apps/meteor/app/api/server/lib/emailInbox.ts @@ -2,15 +2,12 @@ import type { IEmailInbox } from '@rocket.chat/core-typings'; import type { Filter, InsertOneResult, Sort, UpdateResult, WithId } from 'mongodb'; import { EmailInbox } from '@rocket.chat/models'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Users } from '../../../models/server'; export const findEmailInboxes = async ({ - userId, query = {}, pagination: { offset, count, sort }, }: { - userId: string; query?: Filter<IEmailInbox>; pagination: { offset: number; @@ -23,9 +20,6 @@ export const findEmailInboxes = async ({ count: number; offset: number; }> => { - if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { - throw new Error('error-not-allowed'); - } const { cursor, totalCount } = EmailInbox.findPaginated(query, { sort: sort || { name: 1 }, skip: offset, @@ -42,10 +36,7 @@ export const findEmailInboxes = async ({ }; }; -export const findOneEmailInbox = async ({ userId, _id }: { userId: string; _id: string }): Promise<IEmailInbox | null> => { - if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { - throw new Error('error-not-allowed'); - } +export const findOneEmailInbox = async ({ _id }: { _id: string }): Promise<IEmailInbox | null> => { return EmailInbox.findOneById(_id); }; export const insertOneEmailInbox = async ( @@ -62,12 +53,11 @@ export const insertOneEmailInbox = async ( }; export const updateEmailInbox = async ( - userId: string, emailInboxParams: Pick<IEmailInbox, '_id' | 'active' | 'name' | 'email' | 'description' | 'senderInfo' | 'department' | 'smtp' | 'imap'>, ): Promise<InsertOneResult<WithId<IEmailInbox>> | UpdateResult> => { const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; - const emailInbox = await findOneEmailInbox({ userId, _id }); + const emailInbox = await findOneEmailInbox({ _id }); if (!emailInbox) { throw new Error('error-invalid-email-inbox'); @@ -90,10 +80,3 @@ export const updateEmailInbox = async ( return EmailInbox.updateOne({ _id }, updateEmailInbox); }; - -export const findOneEmailInboxByEmail = async ({ userId, email }: { userId: string; email: string }): Promise<IEmailInbox | null> => { - if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { - throw new Error('error-not-allowed'); - } - return EmailInbox.findOne({ email }); -}; diff --git a/apps/meteor/app/api/server/v1/email-inbox.ts b/apps/meteor/app/api/server/v1/email-inbox.ts index 293ae2e6b8a..92d32c14658 100644 --- a/apps/meteor/app/api/server/v1/email-inbox.ts +++ b/apps/meteor/app/api/server/v1/email-inbox.ts @@ -3,18 +3,17 @@ import { EmailInbox } from '@rocket.chat/models'; import { API } from '../api'; import { insertOneEmailInbox, findEmailInboxes, findOneEmailInbox, updateEmailInbox } from '../lib/emailInbox'; -import { hasPermission } from '../../../authorization/server/functions/hasPermission'; import Users from '../../../models/server/models/Users'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; API.v1.addRoute( 'email-inbox.list', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-email-inbox'] }, { async get() { const { offset, count } = this.getPaginationItems(); const { sort, query } = this.parseJsonQuery(); - const emailInboxes = await findEmailInboxes({ userId: this.userId, query, pagination: { offset, count, sort } }); + const emailInboxes = await findEmailInboxes({ query, pagination: { offset, count, sort } }); return API.v1.success(emailInboxes); }, @@ -23,13 +22,9 @@ API.v1.addRoute( API.v1.addRoute( 'email-inbox', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-email-inbox'] }, { async post() { - if (!hasPermission(this.userId, 'manage-email-inbox')) { - throw new Error('error-not-allowed'); - } - check(this.bodyParams, { _id: Match.Maybe(String), active: Boolean, @@ -64,7 +59,7 @@ API.v1.addRoute( _id = emailInbox.insertedId.toString(); } else { _id = emailInboxParams._id; - await updateEmailInbox(this.userId, { ...emailInboxParams, _id }); + await updateEmailInbox({ ...emailInboxParams, _id }); } return API.v1.success({ _id }); }, @@ -73,7 +68,7 @@ API.v1.addRoute( API.v1.addRoute( 'email-inbox/:_id', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-email-inbox'] }, { async get() { check(this.urlParams, { @@ -84,15 +79,11 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - // TODO: Chapter day backend - check if user has permission to view this email inbox instead of null values - const emailInboxes = await findOneEmailInbox({ userId: this.userId, _id }); + const emailInboxes = await findOneEmailInbox({ _id }); return API.v1.success(emailInboxes); }, async delete() { - if (!hasPermission(this.userId, 'manage-email-inbox')) { - throw new Error('error-not-allowed'); - } check(this.urlParams, { _id: String, }); @@ -103,7 +94,6 @@ API.v1.addRoute( } const emailInboxes = await EmailInbox.findOneById(_id); - if (!emailInboxes) { return API.v1.notFound(); } @@ -115,12 +105,9 @@ API.v1.addRoute( API.v1.addRoute( 'email-inbox.search', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-email-inbox'] }, { async get() { - if (!hasPermission(this.userId, 'manage-email-inbox')) { - throw new Error('error-not-allowed'); - } check(this.queryParams, { email: String, }); @@ -138,12 +125,9 @@ API.v1.addRoute( API.v1.addRoute( 'email-inbox.send-test/:_id', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-email-inbox'] }, { async post() { - if (!hasPermission(this.userId, 'manage-email-inbox')) { - throw new Error('error-not-allowed'); - } check(this.urlParams, { _id: String, }); @@ -152,7 +136,7 @@ API.v1.addRoute( if (!_id) { throw new Error('error-invalid-param'); } - const emailInbox = await findOneEmailInbox({ userId: this.userId, _id }); + const emailInbox = await findOneEmailInbox({ _id }); if (!emailInbox) { return API.v1.notFound(); diff --git a/apps/meteor/app/api/server/v1/voip/events.ts b/apps/meteor/app/api/server/v1/voip/events.ts index 5b3d61a46f6..426a1495a5e 100644 --- a/apps/meteor/app/api/server/v1/voip/events.ts +++ b/apps/meteor/app/api/server/v1/voip/events.ts @@ -8,7 +8,7 @@ import { canAccessRoom } from '../../../../authorization/server'; API.v1.addRoute( 'voip/events', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-l-room'] }, { async post() { check(this.requestParams(), { diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index 9e7fe2e0066..06bcdfd6ee6 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -3,7 +3,6 @@ import type { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typin import { Users } from '@rocket.chat/models'; import { API } from '../../api'; -import { hasPermission } from '../../../../authorization/server/index'; import { LivechatVoip } from '../../../../../server/sdk'; import { logger } from './logger'; @@ -31,12 +30,9 @@ const isUserIdndTypeParams = (p: any): p is { userId: string; type: 'free' | 'al API.v1.addRoute( 'omnichannel/agent/extension', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-agent-extension-association'] }, { async post() { - if (!hasPermission(this.userId, 'manage-agent-extension-association')) { - return API.v1.unauthorized(); - } check( this.bodyParams, Match.OneOf( @@ -94,13 +90,16 @@ API.v1.addRoute( API.v1.addRoute( 'omnichannel/agent/extension/:username', - { authRequired: true }, + { + authRequired: true, + permissionsRequired: { + GET: ['view-agent-extension-association'], + DELETE: ['manage-agent-extension-association'], + }, + }, { // Get the extensions associated with the agent passed as request params. async get() { - if (!hasPermission(this.userId, 'view-agent-extension-association')) { - return API.v1.unauthorized(); - } check( this.urlParams, Match.ObjectIncluding({ @@ -128,9 +127,6 @@ API.v1.addRoute( }, async delete() { - if (!hasPermission(this.userId, 'manage-agent-extension-association')) { - return API.v1.unauthorized(); - } check( this.urlParams, Match.ObjectIncluding({ diff --git a/apps/meteor/app/api/server/v1/voip/queues.ts b/apps/meteor/app/api/server/v1/voip/queues.ts index 803418f06c0..845b448b3fa 100644 --- a/apps/meteor/app/api/server/v1/voip/queues.ts +++ b/apps/meteor/app/api/server/v1/voip/queues.ts @@ -6,7 +6,7 @@ import { API } from '../../api'; API.v1.addRoute( 'voip/queues.getSummary', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, { async get() { const queueSummary = await Voip.getQueueSummary(); @@ -17,7 +17,7 @@ API.v1.addRoute( API.v1.addRoute( 'voip/queues.getQueuedCallsForThisExtension', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, { async get() { check( @@ -34,7 +34,7 @@ API.v1.addRoute( API.v1.addRoute( 'voip/queues.getMembershipSubscription', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['inbound-voip-calls'] }, { async get() { check( diff --git a/apps/meteor/app/livechat/imports/server/rest/agent.js b/apps/meteor/app/livechat/imports/server/rest/agent.js index fc98c095e5f..e388ed1be72 100644 --- a/apps/meteor/app/livechat/imports/server/rest/agent.js +++ b/apps/meteor/app/livechat/imports/server/rest/agent.js @@ -7,7 +7,7 @@ API.v1.addRoute( 'livechat/agents/:agentId/departments', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - get() { + async get() { check(this.urlParams, { agentId: String, }); @@ -15,13 +15,11 @@ API.v1.addRoute( enabledDepartmentsOnly: Match.Maybe(String), }); - const departments = Promise.await( - findAgentDepartments({ - userId: this.userId, - enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', - agentId: this.urlParams.agentId, - }), - ); + const departments = await findAgentDepartments({ + userId: this.userId, + enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', + agentId: this.urlParams.agentId, + }); return API.v1.success(departments); }, diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.js b/apps/meteor/app/livechat/imports/server/rest/dashboards.js index 672ad71f10e..da4eb12fc95 100644 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.js +++ b/apps/meteor/app/livechat/imports/server/rest/dashboards.js @@ -18,7 +18,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/conversation-totalizers', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -48,7 +48,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/agents-productivity-totalizers', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -78,7 +78,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/chats-totalizers', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -106,7 +106,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/productivity-totalizers', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -137,7 +137,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -165,7 +165,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats-per-agent', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -193,7 +193,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/charts/agents-status', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { const { departmentId } = this.requestParams(); check(departmentId, Match.Maybe(String)); @@ -208,7 +208,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats-per-department', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -236,7 +236,7 @@ API.v1.addRoute( 'livechat/analytics/dashboards/charts/timings', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index c3a652fab1f..3a76c7ce06f 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -99,57 +99,49 @@ API.v1.addRoute( return API.v1.success({ department, agents }); }, - put() { + async put() { const permissionToSave = hasPermission(this.userId, 'manage-livechat-departments'); const permissionToAddAgents = hasPermission(this.userId, 'add-livechat-department-agents'); - try { - check(this.urlParams, { - _id: String, - }); - - check(this.bodyParams, { - department: Object, - agents: Match.Maybe(Array), - }); + check(this.urlParams, { + _id: String, + }); - const { _id } = this.urlParams; - const { department, agents } = this.bodyParams; + check(this.bodyParams, { + department: Object, + agents: Match.Maybe(Array), + }); - let success; - if (permissionToSave) { - success = Livechat.saveDepartment(_id, department); - } + const { _id } = this.urlParams; + const { department, agents } = this.bodyParams; - if (success && agents && permissionToAddAgents) { - success = Livechat.saveDepartmentAgents(_id, { upsert: agents }); - } + let success; + if (permissionToSave) { + success = Livechat.saveDepartment(_id, department); + } - if (success) { - return API.v1.success({ - department: LivechatDepartment.findOneById(_id), - agents: LivechatDepartmentAgents.find({ departmentId: _id }).fetch(), - }); - } + if (success && agents && permissionToAddAgents) { + success = Livechat.saveDepartmentAgents(_id, { upsert: agents }); + } - return API.v1.failure(); - } catch (e) { - return API.v1.failure(e); + if (success) { + return API.v1.success({ + department: LivechatDepartment.findOneById(_id), + agents: LivechatDepartmentAgents.find({ departmentId: _id }).fetch(), + }); } + + return API.v1.failure(); }, delete() { - try { - check(this.urlParams, { - _id: String, - }); + check(this.urlParams, { + _id: String, + }); - if (Livechat.removeDepartment(this.urlParams._id)) { - return API.v1.success(); - } - return API.v1.failure(); - } catch (e) { - return API.v1.failure(e); + if (Livechat.removeDepartment(this.urlParams._id)) { + return API.v1.success(); } + return API.v1.failure(); }, }, ); @@ -205,7 +197,7 @@ API.v1.addRoute( return API.v1.success(agents); }, - post() { + async post() { check(this.urlParams, { departmentId: String, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.js b/apps/meteor/app/livechat/imports/server/rest/inquiries.js index cff351cad06..6cd4440ec35 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.js +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.js @@ -51,7 +51,7 @@ API.v1.addRoute( 'livechat/inquiries.take', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - post() { + async post() { check(this.bodyParams, { inquiryId: String, userId: Match.Maybe(String), @@ -72,24 +72,22 @@ API.v1.addRoute( 'livechat/inquiries.queued', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); const { department } = this.requestParams(); return API.v1.success( - Promise.await( - findInquiries({ - userId: this.userId, - department, - status: 'queued', - pagination: { - offset, - count, - sort, - }, - }), - ), + await findInquiries({ + userId: this.userId, + department, + status: 'queued', + pagination: { + offset, + count, + sort, + }, + }), ); }, }, diff --git a/apps/meteor/app/livechat/imports/server/rest/users.js b/apps/meteor/app/livechat/imports/server/rest/users.js index 8d1bdc9af13..39b9931defc 100644 --- a/apps/meteor/app/livechat/imports/server/rest/users.js +++ b/apps/meteor/app/livechat/imports/server/rest/users.js @@ -62,7 +62,7 @@ API.v1.addRoute( } throw new Error('Invalid type'); }, - post() { + async post() { check(this.urlParams, { type: String, }); @@ -94,7 +94,7 @@ API.v1.addRoute( 'livechat/users/:type/:_id', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { + async get() { check(this.urlParams, { type: String, _id: String, @@ -126,7 +126,7 @@ API.v1.addRoute( user: null, }); }, - delete() { + async delete() { check(this.urlParams, { type: String, _id: String, diff --git a/apps/meteor/app/livechat/server/api/v1/agent.js b/apps/meteor/app/livechat/server/api/v1/agent.js index 1c33d8fbd16..b08a52dcbf9 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.js +++ b/apps/meteor/app/livechat/server/api/v1/agent.js @@ -32,7 +32,7 @@ API.v1.addRoute('livechat/agent.info/:rid/:token', { }); API.v1.addRoute('livechat/agent.next/:token', { - get() { + async get() { check(this.urlParams, { token: String, }); @@ -55,7 +55,7 @@ API.v1.addRoute('livechat/agent.next/:token', { } } - const agentData = Promise.await(Livechat.getNextAgent(department)); + const agentData = await Livechat.getNextAgent(department); if (!agentData) { throw new Meteor.Error('agent-not-found'); } diff --git a/apps/meteor/app/livechat/server/api/v1/message.js b/apps/meteor/app/livechat/server/api/v1/message.js index 1a2eb12e478..e33135674a7 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.js +++ b/apps/meteor/app/livechat/server/api/v1/message.js @@ -14,69 +14,65 @@ import { settings } from '../../../../settings/server'; API.v1.addRoute('livechat/message', { async post() { - try { - check(this.bodyParams, { - _id: Match.Maybe(String), - token: String, - rid: String, - msg: String, - agent: Match.Maybe({ - agentId: String, - username: String, - }), - }); + check(this.bodyParams, { + _id: Match.Maybe(String), + token: String, + rid: String, + msg: String, + agent: Match.Maybe({ + agentId: String, + username: String, + }), + }); - const { token, rid, agent, msg } = this.bodyParams; + const { token, rid, agent, msg } = this.bodyParams; - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } + const guest = await findGuest(token); + if (!guest) { + throw new Meteor.Error('invalid-token'); + } - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const room = findRoom(token, rid); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - if (!room.open) { - throw new Meteor.Error('room-closed'); - } + if (!room.open) { + throw new Meteor.Error('room-closed'); + } - if ( - settings.get('Livechat_enable_message_character_limit') && - msg.length > parseInt(settings.get('Livechat_message_character_limit')) - ) { - throw new Meteor.Error('message-length-exceeds-character-limit'); - } + if ( + settings.get('Livechat_enable_message_character_limit') && + msg.length > parseInt(settings.get('Livechat_message_character_limit')) + ) { + throw new Meteor.Error('message-length-exceeds-character-limit'); + } - const _id = this.bodyParams._id || Random.id(); + const _id = this.bodyParams._id || Random.id(); - const sendMessage = { - guest, - message: { - _id, - rid, - msg, - token, - }, - agent, - roomInfo: { - source: { - type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, + const sendMessage = { + guest, + message: { + _id, + rid, + msg, + token, + }, + agent, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, }, - }; - - const result = await Livechat.sendMessage(sendMessage); - if (result) { - const message = Messages.findOneById(_id); - return API.v1.success({ message }); - } + }, + }; - return API.v1.failure(); - } catch (e) { - return API.v1.failure(e); + const result = await Livechat.sendMessage(sendMessage); + if (result) { + const message = Messages.findOneById(_id); + return API.v1.success({ message }); } + + return API.v1.failure(); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.js b/apps/meteor/app/livechat/server/api/v1/offlineMessage.js index 8264228c97e..31a5bd8b4d6 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.js +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.js @@ -5,24 +5,20 @@ import { API } from '../../../../api/server'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/offline.message', { - post() { - try { - check(this.bodyParams, { - name: String, - email: String, - message: String, - department: Match.Maybe(String), - host: Match.Maybe(String), - }); + async post() { + check(this.bodyParams, { + name: String, + email: String, + message: String, + department: Match.Maybe(String), + host: Match.Maybe(String), + }); - const { name, email, message, department, host } = this.bodyParams; - if (!Livechat.sendOfflineMessage({ name, email, message, department, host })) { - return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_offline_message') }); - } - - return API.v1.success({ message: TAPi18n.__('Livechat_offline_message_sent') }); - } catch (e) { - return API.v1.failure(e); + const { name, email, message, department, host } = this.bodyParams; + if (!Livechat.sendOfflineMessage({ name, email, message, department, host })) { + return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_offline_message') }); } + + return API.v1.success({ message: TAPi18n.__('Livechat_offline_message_sent') }); }, }); diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.js b/apps/meteor/app/livechat/server/api/v1/pageVisited.js index ea6c05e2ce7..29ec623af40 100644 --- a/apps/meteor/app/livechat/server/api/v1/pageVisited.js +++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.js @@ -5,7 +5,7 @@ import { API } from '../../../../api/server'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/page.visited', { - post() { + async post() { check(this.bodyParams, { token: String, rid: Match.Maybe(String), diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js index 47c68afabf8..95825c4bfd6 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ b/apps/meteor/app/livechat/server/api/v1/room.js @@ -15,6 +15,8 @@ import { normalizeTransferredByData } from '../../lib/Helper'; import { findVisitorInfo } from '../lib/visitors'; import { canAccessRoom } from '../../../../authorization/server'; import { addUserToRoom } from '../../../../lib/server/functions'; +import { apiDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; +import { deprecationWarning } from '../../../../api/server/helpers/deprecationWarning'; API.v1.addRoute('livechat/room', { async get() { @@ -71,78 +73,77 @@ API.v1.addRoute('livechat/room', { API.v1.addRoute('livechat/room.close', { async post() { - try { - check(this.bodyParams, { - rid: String, - token: String, - }); - - const { rid, token } = this.bodyParams; + check(this.bodyParams, { + rid: String, + token: String, + }); - const visitor = await findGuest(token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } + const { rid, token } = this.bodyParams; - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const visitor = await findGuest(token); + if (!visitor) { + throw new Meteor.Error('invalid-token'); + } - if (!room.open) { - throw new Meteor.Error('room-closed'); - } + const room = findRoom(token, rid); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - const language = rcSettings.get('Language') || 'en'; - const comment = TAPi18n.__('Closed_by_visitor', { lng: language }); + if (!room.open) { + throw new Meteor.Error('room-closed'); + } - if (!Livechat.closeRoom({ visitor, room, comment })) { - return API.v1.failure(); - } + const language = rcSettings.get('Language') || 'en'; + const comment = TAPi18n.__('Closed_by_visitor', { lng: language }); - return API.v1.success({ rid, comment }); - } catch (e) { - return API.v1.failure(e); + if (!Livechat.closeRoom({ visitor, room, comment })) { + return API.v1.failure(); } + + return API.v1.success({ rid, comment }); }, }); API.v1.addRoute('livechat/room.transfer', { async post() { - try { - check(this.bodyParams, { - rid: String, - token: String, - department: String, - }); - - const { rid, token, department } = this.bodyParams; + apiDeprecationLogger.warn('livechat/room.transfer has been deprecated. Use livechat/room.forward instead.'); + check(this.bodyParams, { + rid: String, + token: String, + department: String, + }); - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } + const { rid, token, department } = this.bodyParams; - let room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const guest = await findGuest(token); + if (!guest) { + throw new Meteor.Error('invalid-token'); + } - // update visited page history to not expire - Messages.keepHistoryForToken(token); + let room = findRoom(token, rid); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - const { _id, username, name } = guest; - const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); + // update visited page history to not expire + Messages.keepHistoryForToken(token); - if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { - return API.v1.failure(); - } + const { _id, username, name } = guest; + const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - room = findRoom(token, rid); - return API.v1.success({ room }); - } catch (e) { - return API.v1.failure(e); + if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { + return API.v1.failure(); } + + room = findRoom(token, rid); + return API.v1.success( + deprecationWarning({ + endpoint: 'livechat/room.transfer', + versionWillBeRemoved: '6.0', + response: { room }, + }), + ); }, }); @@ -233,7 +234,7 @@ API.v1.addRoute( 'livechat/room.visitor', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - put() { + async put() { // This endpoint is deprecated and will be removed in future versions. check(this.bodyParams, { rid: String, @@ -243,7 +244,7 @@ API.v1.addRoute( const { rid, newVisitorId, oldVisitorId } = this.bodyParams; - const { visitor } = Promise.await(findVisitorInfo({ userId: this.userId, visitorId: newVisitorId })); + const { visitor } = await findVisitorInfo({ userId: this.userId, visitorId: newVisitorId }); if (!visitor) { throw new Meteor.Error('invalid-visitor'); } @@ -269,7 +270,7 @@ API.v1.addRoute( 'livechat/room.join', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - get() { + async get() { check(this.queryParams, { roomId: String }); const { roomId } = this.queryParams; diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.js index ab3fb0f4578..0ea42b293cd 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.js +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.js @@ -9,65 +9,57 @@ import { API } from '../../../../api/server'; import { settings } from '../lib/livechat'; import { canSendMessage } from '../../../../authorization'; import { Livechat } from '../../lib/Livechat'; -import { Logger } from '../../../../logger'; - -const logger = new Logger('LivechatVideoCallApi'); API.v1.addRoute( 'livechat/webrtc.call', { authRequired: true, permissionsRequired: ['view-l-room'] }, { async get() { - try { - check(this.queryParams, { - rid: Match.Maybe(String), - }); + check(this.queryParams, { + rid: Match.Maybe(String), + }); - const room = canSendMessage(this.queryParams.rid, { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const room = canSendMessage(this.queryParams.rid, { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - const webrtcCallingAllowed = rcSettings.get('WebRTC_Enabled') === true && rcSettings.get('Omnichannel_call_provider') === 'WebRTC'; - if (!webrtcCallingAllowed) { - throw new Meteor.Error('webRTC calling not enabled'); - } + const webrtcCallingAllowed = rcSettings.get('WebRTC_Enabled') === true && rcSettings.get('Omnichannel_call_provider') === 'WebRTC'; + if (!webrtcCallingAllowed) { + throw new Meteor.Error('webRTC calling not enabled'); + } - const config = await settings(); - if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) { - throw new Meteor.Error('invalid-livechat-config'); - } + const config = await settings(); + if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) { + throw new Meteor.Error('invalid-livechat-config'); + } - let { callStatus } = room; + let { callStatus } = room; - if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { - await Settings.incrementValueById('WebRTC_Calls_Count'); - callStatus = 'ringing'; - await Rooms.setCallStatusAndCallStartTime(room._id, callStatus); - await Messages.createWithTypeRoomIdMessageAndUser( - 'livechat_webrtc_video_call', - room._id, - TAPi18n.__('Join_my_room_to_start_the_video_call'), - this.user, - { - actionLinks: config.theme.actionLinks.webrtc, - }, - ); - } - const videoCall = { - rid: room._id, - provider: 'webrtc', - callStatus, - }; - return API.v1.success({ videoCall }); - } catch (e) { - logger.error(e); - return API.v1.failure(e); + if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { + await Settings.incrementValueById('WebRTC_Calls_Count'); + callStatus = 'ringing'; + await Rooms.setCallStatusAndCallStartTime(room._id, callStatus); + await Messages.createWithTypeRoomIdMessageAndUser( + 'livechat_webrtc_video_call', + room._id, + TAPi18n.__('Join_my_room_to_start_the_video_call'), + this.user, + { + actionLinks: config.theme.actionLinks.webrtc, + }, + ); } + const videoCall = { + rid: room._id, + provider: 'webrtc', + callStatus, + }; + return API.v1.success({ videoCall }); }, }, ); @@ -76,41 +68,36 @@ API.v1.addRoute( 'livechat/webrtc.call/:callId', { authRequired: true, permissionsRequired: ['view-l-room'] }, { - put() { - try { - check(this.urlParams, { - callId: String, - }); + async put() { + check(this.urlParams, { + callId: String, + }); - check(this.bodyParams, { - rid: Match.Maybe(String), - status: Match.Maybe(String), - }); + check(this.bodyParams, { + rid: Match.Maybe(String), + status: Match.Maybe(String), + }); - const { callId } = this.urlParams; - const { rid, status } = this.bodyParams; + const { callId } = this.urlParams; + const { rid, status } = this.bodyParams; - const room = canSendMessage(rid, { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }); - if (!room) { - throw new Meteor.Error('invalid-room'); - } + const room = canSendMessage(rid, { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }); + if (!room) { + throw new Meteor.Error('invalid-room'); + } - const call = Promise.await(Messages.findOneById(callId)); - if (!call || call.t !== 'livechat_webrtc_video_call') { - throw new Meteor.Error('invalid-callId'); - } + const call = await Messages.findOneById(callId); + if (!call || call.t !== 'livechat_webrtc_video_call') { + throw new Meteor.Error('invalid-callId'); + } - Livechat.updateCallStatus(callId, rid, status, this.user); + Livechat.updateCallStatus(callId, rid, status, this.user); - return API.v1.success({ status }); - } catch (e) { - logger.error(e); - return API.v1.failure(e); - } + return API.v1.success({ status }); }, }, ); diff --git a/apps/meteor/app/livechat/server/methods/transfer.js b/apps/meteor/app/livechat/server/methods/transfer.js index f9f556386ac..4384d7651c2 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.js +++ b/apps/meteor/app/livechat/server/methods/transfer.js @@ -6,11 +6,13 @@ import { hasPermission } from '../../../authorization/server'; import { LivechatRooms, Subscriptions, Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeTransferredByData } from '../lib/Helper'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; // Deprecated in favor of "livechat/room.forward" endpoint // TODO: Deprecated: Remove in v6.0.0 Meteor.methods({ async 'livechat:transfer'(transferData) { + methodDeprecationLogger.warn('livechat:transfer method is deprecated in favor of "livechat/room.forward" endpoint'); if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' }); } diff --git a/apps/meteor/ee/app/api-enterprise/server/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/canned-responses.js index 99e2d90e579..bcc719ee0b4 100644 --- a/apps/meteor/ee/app/api-enterprise/server/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/canned-responses.js @@ -8,9 +8,9 @@ API.v1.addRoute( 'canned-responses.get', { authRequired: true }, { - get() { + async get() { return API.v1.success({ - responses: Promise.await(findAllCannedResponses({ userId: this.userId })), + responses: await findAllCannedResponses({ userId: this.userId }), }); }, }, @@ -20,30 +20,28 @@ API.v1.addRoute( 'canned-responses', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); const { shortcut, text, scope, tags, departmentId, createdBy } = this.requestParams(); check(shortcut, Match.Maybe(String)); check(text, Match.Maybe(String)); check(tags, Match.Maybe([String])); - const { cannedResponses, total } = Promise.await( - findAllCannedResponsesFilter({ - shortcut, - text, - scope, - tags, - departmentId, - userId: this.userId, - createdBy, - options: { - sort, - offset, - count, - fields, - }, - }), - ); + const { cannedResponses, total } = await findAllCannedResponsesFilter({ + shortcut, + text, + scope, + tags, + departmentId, + userId: this.userId, + createdBy, + options: { + sort, + offset, + count, + fields, + }, + }); return API.v1.success({ cannedResponses, count: cannedResponses.length, @@ -51,7 +49,7 @@ API.v1.addRoute( total, }); }, - post() { + async post() { check(this.bodyParams, { _id: Match.Maybe(String), shortcut: String, @@ -72,7 +70,7 @@ API.v1.addRoute( }); return API.v1.success(); }, - delete() { + async delete() { const { _id } = this.requestParams(); check(_id, String); Meteor.runAsUser(this.userId, () => { @@ -87,16 +85,14 @@ API.v1.addRoute( 'canned-responses/:_id', { authRequired: true }, { - get() { + async get() { const { _id } = this.urlParams; check(_id, String); - const cannedResponse = Promise.await( - findOneCannedResponse({ - userId: this.userId, - _id, - }), - ); + const cannedResponse = await findOneCannedResponse({ + userId: this.userId, + _id, + }); return API.v1.success({ cannedResponse }); }, diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js index 18b5aafe76f..1073750293b 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js @@ -41,7 +41,7 @@ export async function findAllCannedResponses({ userId }) { agentId: userId, }, { - fields: { + projection: { departmentId: 1, }, }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js index 6c019683b67..a52b1b4dbfa 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js @@ -1,7 +1,6 @@ import { Match, check } from 'meteor/check'; import { API } from '../../../../../app/api/server'; -import { hasPermission } from '../../../../../app/authorization/server'; import { findAllAverageServiceTime, findAllServiceTime, @@ -10,12 +9,9 @@ import { API.v1.addRoute( 'livechat/analytics/agents/average-service-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); @@ -49,12 +45,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/total-service-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); @@ -88,12 +81,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/available-for-service-history', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { fullReport } = this.requestParams(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js index 09228006e48..7b160fa2996 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js @@ -1,7 +1,6 @@ import { Match, check } from 'meteor/check'; import { API } from '../../../../../app/api/server'; -import { hasPermission } from '../../../../../app/authorization/server'; import { findAllRooms, findAllAverageServiceTime, @@ -15,12 +14,9 @@ import { API.v1.addRoute( 'livechat/analytics/departments/amount-of-chats', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { answered, departmentId } = this.requestParams(); @@ -59,12 +55,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-service-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -101,12 +94,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-chat-duration-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -143,12 +133,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-service-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -185,12 +172,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-waiting-time', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -227,12 +211,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-transferred-chats', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -269,12 +250,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-abandoned-chats', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); @@ -311,12 +289,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/percentage-abandoned-chats', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } + async get() { const { offset, count } = this.getPaginationItems(); let { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts index 15adb00b8e2..964c6482bec 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts @@ -3,7 +3,7 @@ import { setPriorityToInquiry } from './lib/inquiries'; API.v1.addRoute( 'livechat/inquiry.prioritize', - { authRequired: true }, + { authRequired: true, permissionsRequired: { PUT: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } } }, { async put() { const { roomId, priority } = this.bodyParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js index 351bbe06b5b..a3004579286 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js @@ -1,12 +1,8 @@ import { LivechatInquiry, Users, LivechatPriority } from '@rocket.chat/models'; -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; import { LivechatEnterprise } from '../../lib/LivechatEnterprise'; export async function setPriorityToInquiry({ userId, roomId, priority }) { - if (!(await hasPermissionAsync(userId, 'manage-livechat-priorities')) && !(await hasPermissionAsync(userId, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { status: 1 } }); if (!inquiry || inquiry.status !== 'queued') { throw new Error('error-invalid-inquiry'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js index a401e5f4f58..636063f26f1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js @@ -1,12 +1,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Users } from '@rocket.chat/models'; -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; - -export async function findMonitors({ userId, text, pagination: { offset, count, sort } }) { - if (!(await hasPermissionAsync(userId, 'manage-livechat-monitors'))) { - throw new Error('error-not-authorized'); - } +export async function findMonitors({ text, pagination: { offset, count, sort } }) { const query = {}; if (text) { const filterReg = new RegExp(escapeRegExp(text), 'i'); @@ -39,10 +34,7 @@ export async function findMonitors({ userId, text, pagination: { offset, count, }; } -export async function findMonitorByUsername({ userId, username }) { - if (!(await hasPermissionAsync(userId, 'manage-livechat-monitors'))) { - throw new Error('error-not-authorized'); - } +export async function findMonitorByUsername({ username }) { const user = await Users.findOne( { username }, { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts index e79c2dfa031..e4238e22794 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/priorities.ts @@ -3,10 +3,7 @@ import { LivechatPriority } from '@rocket.chat/models'; import type { ILivechatPriority } from '@rocket.chat/core-typings'; import type { FindOptions } from 'mongodb'; -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; - type FindPrioritiesParams = { - userId: string; text?: string; pagination: { offset: number; @@ -23,21 +20,12 @@ type FindPrioritiesResult = { }; type FindPrioritiesByIdParams = { - userId: string; priorityId: string; }; type FindPrioritiesByIdResult = ILivechatPriority | null; -export async function findPriorities({ - userId, - text, - pagination: { offset, count, sort }, -}: FindPrioritiesParams): Promise<FindPrioritiesResult> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-priorities')) && !(await hasPermissionAsync(userId, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } - +export async function findPriorities({ text, pagination: { offset, count, sort } }: FindPrioritiesParams): Promise<FindPrioritiesResult> { const query = { ...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }), }; @@ -58,9 +46,6 @@ export async function findPriorities({ }; } -export async function findPriorityById({ userId, priorityId }: FindPrioritiesByIdParams): Promise<FindPrioritiesByIdResult> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-priorities')) && !(await hasPermissionAsync(userId, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } +export async function findPriorityById({ priorityId }: FindPrioritiesByIdParams): Promise<FindPrioritiesByIdResult> { return LivechatPriority.findOneById(priorityId); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/tags.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/tags.ts index 82816f66e1c..0ebf4df1553 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/tags.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/tags.ts @@ -3,8 +3,6 @@ import { LivechatTag } from '@rocket.chat/models'; import type { ILivechatTag } from '@rocket.chat/core-typings'; import type { FindOptions } from 'mongodb'; -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; - type FindTagsParams = { userId: string; text?: string; @@ -29,10 +27,7 @@ type FindTagsByIdParams = { type FindTagsByIdResult = ILivechatTag | null; -export async function findTags({ userId, text, pagination: { offset, count, sort } }: FindTagsParams): Promise<FindTagsResult> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-tags')) && !(await hasPermissionAsync(userId, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } +export async function findTags({ text, pagination: { offset, count, sort } }: FindTagsParams): Promise<FindTagsResult> { const query = { ...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }), }; @@ -53,9 +48,6 @@ export async function findTags({ userId, text, pagination: { offset, count, sort }; } -export async function findTagById({ userId, tagId }: FindTagsByIdParams): Promise<FindTagsByIdResult> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-tags')) && !(await hasPermissionAsync(userId, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } +export async function findTagById({ tagId }: FindTagsByIdParams): Promise<FindTagsByIdResult> { return LivechatTag.findOneById(tagId); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/units.ts index 1f476443eeb..032fedda9cc 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/units.ts @@ -3,14 +3,10 @@ import type { IOmnichannelBusinessUnit, ILivechatMonitor } from '@rocket.chat/co import { LivechatUnitMonitors, LivechatUnit } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; -import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; - export async function findUnits({ - userId, text, pagination: { offset, count, sort }, }: { - userId: string; text?: string; pagination: { offset: number; @@ -23,9 +19,6 @@ export async function findUnits({ offset: number; total: number; }> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-units'))) { - throw new Error('error-not-authorized'); - } const filter = text && new RegExp(escapeRegExp(text), 'i'); const query = { ...(text && { $or: [{ name: filter }] }) }; @@ -46,16 +39,10 @@ export async function findUnits({ }; } -export async function findUnitMonitors({ userId, unitId }: { userId: string; unitId: string }): Promise<ILivechatMonitor[]> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-monitors'))) { - throw new Error('error-not-authorized'); - } +export async function findUnitMonitors({ unitId }: { unitId: string }): Promise<ILivechatMonitor[]> { return LivechatUnitMonitors.find({ unitId }).toArray() as Promise<ILivechatMonitor[]>; } -export async function findUnitById({ userId, unitId }: { userId: string; unitId: string }): Promise<IOmnichannelBusinessUnit | null> { - if (!(await hasPermissionAsync(userId, 'manage-livechat-units'))) { - throw new Error('error-not-authorized'); - } +export async function findUnitById({ unitId }: { unitId: string }): Promise<IOmnichannelBusinessUnit | null> { return LivechatUnit.findOneById<IOmnichannelBusinessUnit>(unitId); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts index d28622fbfcb..42b53765e9e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts @@ -5,7 +5,10 @@ import { findMonitors, findMonitorByUsername } from './lib/monitors'; API.v1.addRoute( 'livechat/monitors', - { authRequired: true }, + { + authRequired: true, + permissionsRequired: ['manage-livechat-monitors'], + }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -14,7 +17,6 @@ API.v1.addRoute( return API.v1.success( await findMonitors({ - userId: this.userId, text, pagination: { offset, @@ -29,14 +31,16 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/monitors/:username', - { authRequired: true }, + { + authRequired: true, + permissionsRequired: ['manage-livechat-monitors'], + }, { async get() { const { username } = this.urlParams; return API.v1.success( (await findMonitorByUsername({ - userId: this.userId, username, })) as unknown as ILivechatMonitor, ); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts index dce9f005856..edd0c12a27b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts @@ -3,7 +3,7 @@ import { findPriorities, findPriorityById } from './lib/priorities'; API.v1.addRoute( 'livechat/priorities', - { authRequired: true }, + { authRequired: true, permissionsRequired: { GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } } }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -12,7 +12,6 @@ API.v1.addRoute( return API.v1.success( await findPriorities({ - userId: this.userId, text, pagination: { offset, @@ -27,13 +26,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/priorities/:priorityId', - { authRequired: true }, + { authRequired: true, permissionsRequired: { GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } } }, { async get() { const { priorityId } = this.urlParams; const priority = await findPriorityById({ - userId: this.userId, priorityId, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts index 7ad95f90058..ff4e1403792 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts @@ -9,7 +9,7 @@ import { LivechatEnterprise } from '../lib/LivechatEnterprise'; API.v1.addRoute( 'livechat/room.onHold', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['on-hold-livechat-room'] }, { post() { const { roomId } = this.bodyParams; @@ -17,10 +17,6 @@ API.v1.addRoute( return API.v1.failure('Invalid room Id'); } - if (!this.userId || !hasPermission(this.userId, 'on-hold-livechat-room')) { - return API.v1.failure('Not authorized'); - } - const room: IOmnichannelRoom = LivechatRooms.findOneById(roomId); if (!room || room.t !== 'l') { return API.v1.failure('Invalid room Id'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts index 790366f22d8..c64c8a70366 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts @@ -3,7 +3,7 @@ import { findTags, findTagById } from './lib/tags'; API.v1.addRoute( 'livechat/tags', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-l-room', 'manage-livechat-tags'] }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -27,7 +27,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/tags/:tagId', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-l-room', 'manage-livechat-tags'] }, { async get() { const { tagId } = this.urlParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts index 930b6f554ad..cac122fa83e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts @@ -5,7 +5,7 @@ import { findAllDepartmentsAvailable, findAllDepartmentsByUnit } from '../lib/De API.v1.addRoute( 'livechat/units/:unitId/monitors', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-livechat-monitors'] }, { async get() { const { unitId } = this.urlParams; @@ -15,7 +15,6 @@ API.v1.addRoute( } return API.v1.success({ monitors: await findUnitMonitors({ - userId: this.userId, unitId, }), }); @@ -34,7 +33,6 @@ API.v1.addRoute( return API.v1.success( await findUnits({ - userId: this.userId, text, pagination: { offset, @@ -46,7 +44,7 @@ API.v1.addRoute( }, async post() { const { unitData, unitMonitors, unitDepartments } = this.bodyParams; - return LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments); + return API.v1.success(LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments)); }, }, ); @@ -58,7 +56,6 @@ API.v1.addRoute( async get() { const { id } = this.urlParams; const unit = await findUnitById({ - userId: this.userId, unitId: id, }); @@ -80,7 +77,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -100,7 +97,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments/available', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, { async get() { const { offset, count } = this.getPaginationItems(); diff --git a/apps/meteor/tests/data/livechat/inboxes.ts b/apps/meteor/tests/data/livechat/inboxes.ts new file mode 100644 index 00000000000..8f2fef1c9f4 --- /dev/null +++ b/apps/meteor/tests/data/livechat/inboxes.ts @@ -0,0 +1,32 @@ +import { getCredentials, api, request, credentials } from '../api-data'; + +export const createEmailInbox = async (): Promise<{ _id: string }> => { + await getCredentials() + const { body } = await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: false, + email: `test${new Date().getTime()}@test.com`, + description: 'test', + senderInfo: 'test', + department: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }); + return body; +} diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index cc5a10d0da2..538e701c8d1 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -292,6 +292,26 @@ describe('LIVECHAT - rooms', function () { }); }); + describe('livechat/room.join', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: '123' }).send().expect(403); + }); + it('should fail if no roomId is present on query params', async () => { + await updatePermission('view-l-room', ['admin', 'livechat-agent']); + await request.get(api('livechat/room.join')).set(credentials).expect(400); + }); + it('should fail if room is present but invalid', async () => { + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: 'invalid' }).send().expect(400); + }); + it('should allow user to join room', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: room._id }).send().expect(200); + }); + }); + describe('livechat/room.close', () => { it('should return an "invalid-token" error when the visitor is not found due to an invalid token', (done) => { request @@ -305,7 +325,7 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('invalid-token'); + expect(res.body.error).to.be.equal('[invalid-token]'); }) .end(done); }); @@ -322,7 +342,7 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('invalid-room'); + expect(res.body.error).to.be.equal('[invalid-room]'); }) .end(done); }); @@ -357,7 +377,7 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('room-closed'); + expect(res.body.error).to.be.equal('[room-closed]'); }) .end(done); }); @@ -998,4 +1018,79 @@ describe('LIVECHAT - rooms', function () { expect(body.messages[1]).to.have.property('username', visitor.username); }); }); + describe('livechat/transfer.history/:rid', () => { + it('should fail if user doesnt have "view-livechat-rooms" permission', async () => { + await updatePermission('view-livechat-rooms', []); + const { body } = await request + .get(api(`livechat/transfer.history/test`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + expect(body).to.have.property('success', false); + }); + it('should fail if room is not a valid room id', async () => { + await updatePermission('view-livechat-rooms', ['admin', 'livechat-manager']); + const { body } = await request + .get(api(`livechat/transfer.history/test`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + expect(body).to.have.property('success', false); + }); + it('should return empty for a room without transfer history', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const { body } = await request + .get(api(`livechat/transfer.history/${room._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(body).to.have.property('success', true); + expect(body).to.have.property('history').that.is.an('array'); + expect(body.history.length).to.equal(0); + }); + it('should return the transfer history for a room', async () => { + const initialAgentAssignedToChat: IUser = await createUser(); + const initialAgentCredentials = await login(initialAgentAssignedToChat.username, password); + await createAgent(initialAgentAssignedToChat.username); + await makeAgentAvailable(initialAgentCredentials); + + const newVisitor = await createVisitor(); + // at this point, the chat will get transferred to agent "user" + const newRoom = await createLivechatRoom(newVisitor.token); + + const forwardChatToUser: IUser = await createUser(); + const forwardChatToUserCredentials = await login(forwardChatToUser.username, password); + await createAgent(forwardChatToUser.username); + await makeAgentAvailable(forwardChatToUserCredentials); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId: newRoom._id, + userId: forwardChatToUser._id, + clientAction: true, + comment: 'test comment', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + const { body } = await request + .get(api(`livechat/transfer.history/${newRoom._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('success', true); + expect(body).to.have.property('history').that.is.an('array'); + expect(body.history.length).to.equal(1); + expect(body.history[0]).to.have.property('scope', 'agent'); + expect(body.history[0]).to.have.property('comment', 'test comment'); + expect(body.history[0]).to.have.property('transferredBy').that.is.an('object'); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts index 2cb4e6e3db4..eef54ed55ea 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts @@ -4,8 +4,8 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createDepartment } from '../../../data/livechat/rooms'; - -// TODO: Add tests with actual e-mail servers involved +import { createEmailInbox } from '../../../data/livechat/inboxes'; +import { updatePermission } from '../../../data/permissions.helper'; describe('Email inbox', () => { before((done) => getCredentials(done)); @@ -66,28 +66,330 @@ describe('Email inbox', () => { done(); }); describe('GET email-inbox.list', () => { - it('should return a list of email inboxes', (done) => { - request - .get(api('email-inbox.list')) + it('should fail if user doesnt have manage-email-inbox permission', async () => { + await updatePermission('manage-email-inbox', []); + await request.get(api('email-inbox.list')).set(credentials).expect(403); + }); + it('should return a list of email inboxes', async () => { + await updatePermission('manage-email-inbox', ['admin']); + const res = await request.get(api('email-inbox.list')).set(credentials).send().expect('Content-Type', 'application/json').expect(200); + + expect(res.body).to.have.property('emailInboxes'); + expect(res.body.emailInboxes).to.be.an('array'); + expect(res.body.emailInboxes).to.have.length.of.at.least(1); + expect(res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')).to.have.length.gte(1); + // make sure we delete the test inbox, even if creation failed on this test run + testInbox = res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')[0]._id; + expect(res.body).to.have.property('total'); + expect(res.body.total).to.be.a('number'); + expect(res.body).to.have.property('count'); + expect(res.body.count).to.be.a('number'); + expect(res.body).to.have.property('offset'); + expect(res.body.offset).to.be.a('number'); + }); + }); + + describe('POST email-inbox', () => { + let inboxId: string; + it('should fail if user doesnt have manage-email-inbox permission', async () => { + await updatePermission('manage-email-inbox', []); + await request.post(api('email-inbox')).set(credentials).send({}).expect(403); + }); + it('should fail if smtp config is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request.post(api('email-inbox')).set(credentials).send({}).expect(400); + }); + it('should fail if imap config is not on body params', async () => { + await request + .post(api('email-inbox')) .set(credentials) - .send() - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('emailInboxes'); - expect(res.body.emailInboxes).to.be.an('array'); - expect(res.body.emailInboxes).to.have.length.of.at.least(1); - expect(res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')).to.have.length.gte(1); - // make sure we delete the test inbox, even if creation failed on this test run - testInbox = res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')[0]._id; - expect(res.body).to.have.property('total'); - expect(res.body.total).to.be.a('number'); - expect(res.body).to.have.property('count'); - expect(res.body.count).to.be.a('number'); - expect(res.body).to.have.property('offset'); - expect(res.body.offset).to.be.a('number'); + .send({ + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, }) - .end(() => done()); + .expect(400); + }); + it('should fail if name is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should fail if active is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should fail if email is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: true, + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should fail if description is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: true, + email: 'test@test.com', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should fail if senderInfo is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: true, + email: 'test@test.com', + description: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should fail if department is not on body params', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: true, + email: 'test@test.com', + description: 'test', + senderInfo: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(400); + }); + it('should save an email inbox', async () => { + await updatePermission('manage-email-inbox', ['admin']); + const { body } = await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: false, + email: `test${new Date().getTime()}@test.com`, + description: 'test', + senderInfo: 'test', + department: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(200); + + expect(body).to.have.property('_id'); + inboxId = body._id; + }); + it('should update an email inbox when _id is passed in the object', async () => { + await updatePermission('manage-email-inbox', ['admin']); + const { body } = await request + .post(api('email-inbox')) + .set(credentials) + .send({ + _id: inboxId, + name: 'test', + active: false, + email: `test${new Date().getTime()}@test.com`, + description: 'Updated test description', + senderInfo: 'test', + department: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }) + .expect(200); + + expect(body).to.have.property('_id'); + }); + }); + describe('GET email-inbox/:_id', () => { + it('should fail if user doesnt have manage-email-inbox permission', async () => { + await updatePermission('manage-email-inbox', []); + await request.get(api('email-inbox/123')).set(credentials).expect(403); + }); + it('should return nothing when email inbox does not exist', async () => { + await updatePermission('manage-email-inbox', ['admin']); + const { body } = await request.get(api('email-inbox/123')).set(credentials).expect(200); + + expect(body.body).to.be.null; + }); + it('should return an email inbox', async () => { + const inbox = await createEmailInbox(); + const { body } = await request + .get(api(`email-inbox/${inbox._id}`)) + .set(credentials) + .expect(200); + + expect(body).to.have.property('_id'); + expect(body).to.have.property('name', 'test'); + }); + }); + describe('DELETE email-inbox/:_id', () => { + it('should fail if user doesnt have manage-email-inbox permission', async () => { + await updatePermission('manage-email-inbox', []); + await request.delete(api('email-inbox/123')).set(credentials).expect(403); + }); + it('should return nothing when email inbox does not exist', async () => { + await updatePermission('manage-email-inbox', ['admin']); + await request.delete(api('email-inbox/123')).set(credentials).expect(404); + }); + it('should delete an email inbox', async () => { + const inbox = await createEmailInbox(); + const { body } = await request + .delete(api(`email-inbox/${inbox._id}`)) + .set(credentials) + .expect(200); + + expect(body).to.have.property('success', true); + }); + }); + describe('GET email-inbox.search', () => { + it('should fail if user doesnt have manage-email-inbox permission', async () => { + await updatePermission('manage-email-inbox', []); + await request.get(api('email-inbox.search')).set(credentials).expect(403); + }); + it('should return an email inbox matching email', async () => { + await createEmailInbox(); + await updatePermission('manage-email-inbox', ['admin']); + await request.get(api(`email-inbox.search?email=test`)).set(credentials).expect(200); }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index aab986d32d5..eae77c01e93 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -186,4 +186,44 @@ describe('LIVECHAT - Utils', function () { expect(body).to.have.property('success', true); }); }); + describe('livechat/visitor.callStatus', () => { + it('should fail if token is not in body params', async () => { + const { body } = await request.post(api('livechat/visitor.callStatus')).set(credentials).send({}); + expect(body).to.have.property('success', false); + }); + it('should fail if rid is not in body params', async () => { + const { body } = await request.post(api('livechat/visitor.callStatus')).set(credentials).send({ token: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should fail if callStatus is not in body params', async () => { + const { body } = await request.post(api('livechat/visitor.callStatus')).set(credentials).send({ token: 'test', rid: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should fail if callId is not in body params', async () => { + const { body } = await request + .post(api('livechat/visitor.callStatus')) + .set(credentials) + .send({ token: 'test', rid: 'test', callStatus: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should fail if token is not a valid guest token', async () => { + const { body } = await request + .post(api('livechat/visitor.callStatus')) + .set(credentials) + .send({ token: new Date().getTime(), rid: 'test', callStatus: 'test', callId: 'test' }); + expect(body).to.have.property('success', false); + }); + it('should try update a call status on room', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/visitor.callStatus')) + .set(credentials) + .send({ token: visitor.token, rid: room._id, callStatus: 'going', callId: 'test' }); + expect(body).to.have.property('success', true); + expect(body).to.have.property('callStatus', 'going'); + expect(body).to.have.property('token', visitor.token); + }); + }); }); -- GitLab From 5fdf20b6486c5f505553d5e263fc69dc3ecb8e87 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 9 Sep 2022 15:34:31 -0300 Subject: [PATCH 019/107] Chore: first non-aggressive CSS removal (#26714) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../client/views/DiscussionTabbar.html | 3 - .../livechat/client/stylesheets/livechat.css | 59 +- .../theme/client/imports/components/badge.css | 39 -- .../theme/client/imports/components/chip.css | 24 - .../imports/components/contextual-bar.css | 73 -- .../components/modal/create-channel.css | 65 -- .../client/imports/forms/select-avatar.css | 72 -- .../theme/client/imports/general/base_old.css | 622 +----------------- .../theme/client/imports/general/forms.css | 48 -- .../app/theme/client/imports/general/rtl.css | 31 - .../client/imports/general/theme_old.css | 55 +- apps/meteor/app/theme/client/main.css | 8 - .../meteor/app/ui/client/components/tabs.html | 16 - apps/meteor/app/ui/client/components/tabs.js | 31 - apps/meteor/app/ui/client/index.ts | 3 +- 15 files changed, 8 insertions(+), 1141 deletions(-) delete mode 100644 apps/meteor/app/discussion/client/views/DiscussionTabbar.html delete mode 100644 apps/meteor/app/theme/client/imports/components/badge.css delete mode 100644 apps/meteor/app/theme/client/imports/components/chip.css delete mode 100644 apps/meteor/app/theme/client/imports/components/modal/create-channel.css delete mode 100644 apps/meteor/app/theme/client/imports/forms/select-avatar.css delete mode 100644 apps/meteor/app/ui/client/components/tabs.html delete mode 100644 apps/meteor/app/ui/client/components/tabs.js diff --git a/apps/meteor/app/discussion/client/views/DiscussionTabbar.html b/apps/meteor/app/discussion/client/views/DiscussionTabbar.html deleted file mode 100644 index 80f65ec9d16..00000000000 --- a/apps/meteor/app/discussion/client/views/DiscussionTabbar.html +++ /dev/null @@ -1,3 +0,0 @@ -<template name="discussionsTabbar"> - {{ > DiscussionMessageList rid=rid onClose=close}} -</template> diff --git a/apps/meteor/app/livechat/client/stylesheets/livechat.css b/apps/meteor/app/livechat/client/stylesheets/livechat.css index 53fb8737b7d..c390e2bc706 100644 --- a/apps/meteor/app/livechat/client/stylesheets/livechat.css +++ b/apps/meteor/app/livechat/client/stylesheets/livechat.css @@ -43,41 +43,6 @@ padding: 0 20px; } -.visitor-navigation, -.visitor-custom-fields { - .visitor-scroll { - overflow-y: auto; - - height: 130px; - margin-top: 4px; - margin-bottom: 20px; - padding: 4px; - - border: 1px solid #e7e7e7; - border-radius: 4px; - - ul { - li { - white-space: nowrap; - - a { - display: block; - overflow: hidden; - - text-decoration: underline; - text-overflow: ellipsis; - - color: var(--secondary-font-color); - - &:hover { - text-decoration: none; - } - } - } - } - } -} - .livechat-status { color: #9d9fa3; @@ -96,34 +61,12 @@ bottom: 0; left: 10px; - content: " "; + content: ' '; border-bottom: 1px solid #cccccc; } } -.user-view { - li { - color: var(--secondary-font-color); - - font-size: 12px; - font-weight: 300; - line-height: 18px; - } - - nav.centered-buttons { - margin-bottom: 1em; - - text-align: center; - - button { - display: inline-block; - - width: auto; - } - } -} - .visitor-edit { h3 { margin-bottom: 8px; diff --git a/apps/meteor/app/theme/client/imports/components/badge.css b/apps/meteor/app/theme/client/imports/components/badge.css deleted file mode 100644 index cbfac82b1a9..00000000000 --- a/apps/meteor/app/theme/client/imports/components/badge.css +++ /dev/null @@ -1,39 +0,0 @@ -.badge { - display: flex; - - min-width: 18px; - min-height: 18px; - margin: 0 3px; - padding: 2px 5px; - - color: var(--badge-text-color); - border-radius: var(--badge-radius); - background-color: var(--badge-background); - - font-size: var(--badge-text-size); - line-height: 1; - align-items: center; - justify-content: center; - - &--unread { - white-space: nowrap; - - background-color: var(--badge-unread-background); - } - - &--thread { - margin: 0 3px; - - white-space: nowrap; - - background-color: var(--badge-unread-background); - } - - &--user-mentions { - background-color: var(--badge-user-mentions-background); - } - - &--group-mentions { - background-color: var(--badge-group-mentions-background); - } -} diff --git a/apps/meteor/app/theme/client/imports/components/chip.css b/apps/meteor/app/theme/client/imports/components/chip.css deleted file mode 100644 index 8c475bd1e8f..00000000000 --- a/apps/meteor/app/theme/client/imports/components/chip.css +++ /dev/null @@ -1,24 +0,0 @@ -.chip-container { - list-style-type: none; -} - -.chip-container li, -.chip-container__item { - display: inline-block; - - margin: 1px 0; - padding: 8px; - - border-radius: 2px; - background-color: var(--chip-background); - - & > .icon { - width: 24px; - } -} - -.chip-container .icon-plus-circled { - opacity: 0.5; - - font-size: 0.8rem; -} diff --git a/apps/meteor/app/theme/client/imports/components/contextual-bar.css b/apps/meteor/app/theme/client/imports/components/contextual-bar.css index 6ce4092d576..8f54ce6b955 100644 --- a/apps/meteor/app/theme/client/imports/components/contextual-bar.css +++ b/apps/meteor/app/theme/client/imports/components/contextual-bar.css @@ -193,76 +193,3 @@ animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95); } } - -.contextual-bar__content.mail-messages { - & .rc-popup-list { - z-index: 1; - } - - & .rc-input:not(:last-child) { - margin-bottom: 2rem; - } -} - -.mail-messages__instructions { - display: flex; - - margin-bottom: 3rem; - - padding: 1.25rem; - - color: var(--rc-color-alert-message-primary); - - border-width: 1px; - border-color: var(--rc-color-alert-message-primary); - border-radius: 2px; - background: var(--rc-color-alert-message-primary-background); - - &--selected { - cursor: pointer; - - color: var(--rc-color-alert-message-secondary); - border-color: var(--rc-color-alert-message-secondary); - background: var(--rc-color-alert-message-secondary-background); - } - - &--warning { - color: var(--rc-color-alert-message-warning); - border-color: var(--rc-color-alert-message-warning); - background: var(--rc-color-alert-message-warning-background); - } - - &-wrapper { - display: flex; - - margin: 0 -10px; - } - - &-icon { - margin: 0 10px; - - font-size: 2rem; - - &--hand-pointer { - margin: 0 15px; - - transform: rotate3d(0, 0, 1, -25deg); - fill: currentColor; - } - } - - &-text { - display: flex; - flex-direction: column; - - margin: 0 10px; - - font-size: 0.875rem; - font-weight: 600; - line-height: 1.2rem; - - &-selected { - font-weight: 400; - } - } -} diff --git a/apps/meteor/app/theme/client/imports/components/modal/create-channel.css b/apps/meteor/app/theme/client/imports/components/modal/create-channel.css deleted file mode 100644 index f9c5a8be28c..00000000000 --- a/apps/meteor/app/theme/client/imports/components/modal/create-channel.css +++ /dev/null @@ -1,65 +0,0 @@ -.create-channel { - display: flex; - flex-direction: column; - - width: 100%; - - animation-name: fadeIn; - animation-duration: 1s; - - &__content { - overflow: auto; - flex: 1 1 auto; - - margin: 0 -40px; - padding: 0 40px; - } - - &__wrapper { - display: flex; - flex-direction: column; - - height: 100%; - } - - &__switches, - &__inputs:not(:only-of-type), - & .rc-input:not(:only-of-type) { - margin-bottom: var(--create-channel-gap-between-elements); - } - - &__description { - padding: var(--create-channel-gap-between-elements) 0; - - color: var(--create-channel-description-color); - - font-size: var(--create-channel-description-text-size); - } - - &__switches { - display: flex; - flex-wrap: wrap; - } - - & .rc-switch { - width: 100%; - - &:not(:last-child) { - margin-bottom: 2rem; - } - } - - & .rc-input__icon-svg { - font-size: 1.2rem; - } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} diff --git a/apps/meteor/app/theme/client/imports/forms/select-avatar.css b/apps/meteor/app/theme/client/imports/forms/select-avatar.css deleted file mode 100644 index 41510385bf3..00000000000 --- a/apps/meteor/app/theme/client/imports/forms/select-avatar.css +++ /dev/null @@ -1,72 +0,0 @@ -.rc-select-avatar { - display: flex; - align-items: flex-start; - - &__preview { - flex: 0 0 var(--select-avatar-preview-size); - - width: var(--select-avatar-preview-size); - height: var(--select-avatar-preview-size); - margin-right: 1rem; - - background-color: transparent; - background-image: - linear-gradient(45deg, var(--color-gray) 25%, transparent 25%, transparent 75%, var(--color-gray) 75%, var(--color-gray)), - linear-gradient(45deg, var(--color-gray) 25%, transparent 25%, transparent 75%, var(--color-gray) 75%, var(--color-gray)); - background-position: 0 0, 5px 5px; - background-size: 10px 10px; - } - - &__loading::after { - display: inline-block; - overflow: hidden; - - width: 0; - - content: "\2026"; /* ascii code for the ellipsis character */ - animation: ellipsis steps(4, end) 1.5s infinite; - vertical-align: bottom; - } - - &__list { - display: flex; - flex-wrap: wrap; - - &-item { - width: var(--select-avatar-size); - height: var(--select-avatar-size); - margin-right: 10px; - margin-bottom: 10px; - - &.disabled { - opacity: 0.4; - } - - cursor: pointer; - - & .avatar { - transition: opacity 0.5s; - } - - & .avatar:hover { - opacity: 0.8; - } - } - } - - &__upload-label { - display: block; - - cursor: pointer; - - color: var(--select-avatar-upload-color); - background: var(--select-avatar-upload-background); - } - - &__upload-icon { - width: var(--select-avatar-size); - height: var(--select-avatar-size); - padding: 12px; - fill: currentColor; - } -} diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 2d5b2b86696..23450521d1c 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -2,11 +2,6 @@ overflow: hidden !important; } -.rc-old .page-settings, -.rc-old .page-settings * { - user-select: text; -} - .rc-old code { margin: 5px 0; padding: 0.5em; @@ -19,13 +14,7 @@ border-width: 1px; border-radius: var(--border-radius); - font-family: - Menlo, - Monaco, - Consolas, - "Liberation Mono", - "Courier New", - monospace; + font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 13px; font-weight: 600; direction: ltr; @@ -327,35 +316,6 @@ } } -.rc-old input { - & .input-forward { - visibility: hidden; - - width: 0; - - transition: width 0.5s ease-in; - - & .show { - visibility: visible; - - width: calc(100% - 48px); - } - } - - & .search::before { - position: absolute; - top: 0; - left: 0; - - display: block; - - width: 30px; - height: 30px; - - content: ' '; - } -} - .rc-old form.inline { & input[type='text'], & input[type='number'], @@ -867,210 +827,6 @@ /* MAIN CONTENT + MAIN PAGES */ -.rc-old .page-settings { - & .content { - & h2 { - margin-bottom: 2rem; - } - - & h3 { - margin-bottom: 1rem; - } - - & > .info { - max-width: none; - padding-left: 20px; - - font-size: 16px; - font-weight: 500; - line-height: 24px; - } - } - - & .section { - padding: 20px 0; - - &:not(:only-child) { - border-bottom: 2px solid #dddddd; - - &.section-collapsed .section-content { - display: none; - } - - .section-title-right { - visibility: visible; - } - } - } - - & .section-title { - display: flex; - - cursor: pointer; - - & .section-title-text { - user-select: none; - - font-size: 1rem; - font-weight: 500; - - line-height: 3rem; - - flex-grow: 1; - } - - & .section-title-right { - visibility: hidden; - - & > .rc-button { - font-size: 1.25rem; - } - } - } - - & .section-content { - padding: 20px 0 !important; - - border: none !important; - border-radius: 0 !important; - - p { - margin-bottom: 0.75rem; - } - - & .input-line { - display: flex; - - margin-bottom: 0; - padding: 10px 0; - - align-items: flex-start; - - &:last-child { - padding-bottom: 0; - - border-bottom: none; - } - - & .add-token { - display: flex; - - & .rc-select { - width: 40%; - margin: 0 0 0 10px; - } - } - - &:first-child { - padding-top: 0; - } - - & .horizontal { - display: flex; - } - - & .flex-grow-1 { - flex-grow: 1; - } - } - - & .reset-setting { - margin-left: 0.75rem; - padding: 0.75rem; - } - - & .setting-label { - min-width: 30%; - - font-weight: 500; - line-height: 16px; - } - - .setting-action { - height: auto; - margin: 0 4px; - } - - & .settings-description { - padding-top: 2px; - - line-height: 1.2rem; - } - - & .settings-alert { - margin-top: 0.75rem; - padding: 1rem; - - border-width: 2px; - - font-weight: bold; - - line-height: 1.2rem; - } - - & .selected-rooms .remove-room { - cursor: pointer; - } - } - - & .settings-description { - user-select: text; - } - - & .rocket-form { - width: 100%; - max-width: none; - padding: 0; - } - - & .settings-file-preview { - display: flex; - align-items: center; - - & input[type='file'] { - position: absolute !important; - z-index: 10000; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - cursor: pointer; - - opacity: 0; - - & * { - cursor: pointer; - } - } - - & .preview { - overflow: hidden; - - width: 100px; - height: 40px; - - margin-right: 0.75rem; - - border-width: var(--input-border-width); - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - - &.no-file { - display: flex; - - align-items: center; - justify-content: center; - } - } - } -} - .rc-old .page-list { & .search { margin-bottom: 12px; @@ -1561,12 +1317,7 @@ padding: 1rem; border-width: 1px 0 0; - background: - linear-gradient( - to bottom, - var(--rc-color-alert-message-warning-background) 0%, - rgba(255, 255, 255, 0) 100% - ); + background: linear-gradient(to bottom, var(--rc-color-alert-message-warning-background) 0%, rgba(255, 255, 255, 0) 100%); } } @@ -2206,273 +1957,6 @@ } } -.rc-old .user-view { - z-index: 15; - - overflow-x: hidden; - overflow-y: auto; - - & .about { - width: 100%; - margin-bottom: 20px; - } - - & .thumb { - width: 100%; - height: 350px; - padding: 20px; - } - - & nav { - padding: 0 20px; - - & .back { - float: right; - } - } - - & .info { - padding: 0 20px; - - white-space: normal; - - & h2 { - overflow: hidden; - - width: 100%; - margin: 8px 0; - - user-select: text; - white-space: nowrap; - text-overflow: ellipsis; - - font-size: 24px; - line-height: 27px; - - & i::after { - display: inline-block; - - width: 8px; - height: 8px; - - content: ' '; - vertical-align: middle; - - border-radius: var(--border-radius); - } - } - - & h3 { - margin: 8px 0; - } - - & p { - user-select: text; - - font-size: 12px; - font-weight: 300; - line-height: 18px; - } - } - - & .stats li { - display: inline-block; - - margin-bottom: 3px; - padding: 4px 6px; - - border-right: 2px; - border-radius: 2px; - } - - & .box { - position: relative; - - margin-bottom: 25px; - - font-size: 13px; - - & h4 { - margin-bottom: 6px; - - text-transform: uppercase; - - font-size: 13px; - font-weight: 600; - } - - &::after { - position: absolute; - bottom: -10px; - left: 0; - - width: 100%; - height: 1px; - - content: ' '; - } - } - - & .tags li { - display: inline-block; - - padding: 4px; - - border-right: 2px; - } - - & .links { - & i { - margin-right: 5px; - - font-size: 13px; - } - - & a { - position: relative; - - display: block; - overflow: hidden; - - max-width: 100%; - padding: 0 5px; - - transition: background 0.18s ease, color 0.18s ease; - white-space: nowrap; - text-overflow: ellipsis; - - border-radius: 2px; - - line-height: 22px; - - &::before { - position: absolute; - top: 0; - right: 5px; - - content: attr(data-stats); - - opacity: 0; - - font-size: 11px; - } - - &:hover { - padding-right: 34px; - - text-decoration: none; - - &::before { - opacity: 1; - } - } - - & span { - font-weight: 300; - } - } - } - - & .channels { - & h3 { - margin-bottom: 8px; - - font-size: 24px; - line-height: 22px; - } - - & p { - font-size: 12px; - font-weight: 300; - line-height: 18px; - } - - & a { - position: relative; - - display: block; - overflow: hidden; - - max-width: 100%; - padding: 0 5px; - - transition: background 0.18s ease, color 0.18s ease; - white-space: nowrap; - text-overflow: ellipsis; - - border-radius: 2px; - - line-height: 22px; - - &::before { - position: absolute; - top: 0; - right: 5px; - - content: attr(data-stats); - - opacity: 0; - - font-size: 11px; - } - - & span { - font-weight: 300; - } - } - } -} - -.rc-old .edit-form { - padding: 20px 20px 0; - - white-space: normal; - - & h3 { - margin-bottom: 8px; - - font-size: 24px; - line-height: 22px; - } - - & p { - font-size: 12px; - font-weight: 300; - line-height: 18px; - } - - & > .input-line { - margin-top: 20px; - - & #password { - width: 70%; - } - - & #roleSelect { - width: 70%; - } - } - - & nav { - padding: 0; - - &.buttons { - margin-top: 2em; - } - } - - & .form-divisor { - height: 9px; - margin: 2em 0; - - text-align: center; - - & > span { - padding: 0 1em; - } - } -} - .rc-old #login-card { position: relative; z-index: 1; @@ -2825,104 +2309,6 @@ background-color: var(--selection-background); } -.rc-old .avatar-suggestions { - display: flex; - flex-flow: column nowrap; -} - -.rc-old .avatar-suggestion-item { - display: flex; - - width: 100%; - margin: 5px 0; - padding: 12px; - - transition: background-color 0.15s ease-out, border-color 0.15s ease-out; - text-align: left; - - border-width: 1px; - border-radius: var(--border-radius); - align-items: center; - flex-flow: row nowrap; - - &:first-child { - margin-top: 10px; - } - - & .avatar { - position: relative; - - width: 55px; - min-width: 55px; - max-width: 55px; - height: 55px; - min-height: 55px; - max-height: 55px; - - text-align: center; - - background-size: cover; - - font-size: 40px; - } - - & .question-mark::before { - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - margin: 0; - - line-height: 55px; - } - - & .action { - padding-left: 20px; - - text-align: right; - } - - & .button { - min-width: 120px; - - cursor: pointer; - text-align: center; - } - - & .input-line { - display: flex; - align-items: center; - } - - & #avatarurl { - margin-right: 20px; - } - - & input[type='file'] { - position: absolute !important; - z-index: 10000; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - cursor: pointer; - - opacity: 0; - - & * { - cursor: pointer; - } - } - - & .avatar-file-input::-webkit-file-upload-button { - visibility: hidden; - } -} - .rc-old .dropzone { & .dropzone-overlay { position: absolute; @@ -3406,9 +2792,7 @@ height: 57px; padding-bottom: 8px; - transition: - transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), - visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1); + transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1); border-bottom: 1px solid var(--color-gray-lightest); justify-content: center; diff --git a/apps/meteor/app/theme/client/imports/general/forms.css b/apps/meteor/app/theme/client/imports/general/forms.css index 6e43b0d4baf..e02c81b5b08 100644 --- a/apps/meteor/app/theme/client/imports/general/forms.css +++ b/apps/meteor/app/theme/client/imports/general/forms.css @@ -187,54 +187,6 @@ } } -.preferences-page { - display: flex; - flex-direction: column; - - height: 100%; - - &--new { - padding: 1.5rem; - } - - &__content { - display: flex; - overflow-y: auto; - flex-direction: column; - - flex: 1; - - width: 100%; - padding: 25px 0; - } - - &--apps .preferences-page__header { - justify-content: space-between; - } - - & .container { - flex: 1; - - width: 100%; - } - - &__title { - margin-top: 20px; - margin-bottom: 20px; - } - - &__alert { - margin-bottom: 20px; - } -} - -@media (width > 500px) { - .preferences-page .container { - max-width: 649px; - margin: auto; - } -} - @keyframes spin { 0% { transform: rotate(0deg); diff --git a/apps/meteor/app/theme/client/imports/general/rtl.css b/apps/meteor/app/theme/client/imports/general/rtl.css index 600fb1b5173..e726921f068 100644 --- a/apps/meteor/app/theme/client/imports/general/rtl.css +++ b/apps/meteor/app/theme/client/imports/general/rtl.css @@ -28,21 +28,6 @@ } } - & .page-settings { - & .content > .info { - padding-left: 20px; - } - - & .section { - border-right: none; - border-left: 1px solid #dddddd; - - & .section-content .input-line > label { - text-align: right; - } - } - } - & .messages-box { margin: 60px 0 0; @@ -191,22 +176,6 @@ } } - & .user-view { - & nav { - margin-right: -4px; - margin-left: auto; - - & .back { - float: left; - } - } - - & .stats li { - border-right: unset; - border-left: 2px; - } - } - & .arrow { &::before, &::after { diff --git a/apps/meteor/app/theme/client/imports/general/theme_old.css b/apps/meteor/app/theme/client/imports/general/theme_old.css index 2f799629d83..c279199a460 100644 --- a/apps/meteor/app/theme/client/imports/general/theme_old.css +++ b/apps/meteor/app/theme/client/imports/general/theme_old.css @@ -164,12 +164,6 @@ } } -.input-line { - &.setting-changed > label { - color: var(--selection-color); - } -} - input:-webkit-autofill { color: var(--primary-font-color) !important; background-color: transparent !important; @@ -205,8 +199,8 @@ textarea { background-color: var(--popup-list-selected-background); } -.rc-old input[type="button"], -.rc-old input[type="submit"] { +.rc-old input[type='button'], +.rc-old input[type='submit'] { color: var(--button-secondary-text-color); border-color: var(--button-secondary-background); background: var(--button-secondary-background); @@ -243,7 +237,7 @@ textarea { .message, .flex-tab { a i, - a[class^="icon-"] { + a[class^='icon-'] { color: var(--primary-font-color); &:hover { @@ -256,17 +250,6 @@ textarea { border-color: var(--error-color); } -.page-list, -.page-settings { - a:not(.rc-button) { - color: var(--primary-font-color); - - &:hover { - color: var(--primary-action-color); - } - } -} - .admin-table-row { background-color: var(--transparent-light); @@ -275,12 +258,6 @@ textarea { } } -.avatar-suggestion-item { - .question-mark::before { - color: var(--secondary-font-color); - } -} - .full-page, .page-loading { a { @@ -351,20 +328,6 @@ textarea { } } -.sidebar-item__last-message { - a:not(.mention-link) { - color: var(--link-font-color); - - &:hover { - opacity: 0.6; - } - } -} - -i.status-online { - color: var(--rc-status-online); -} - .status-bg-online { background-color: var(--rc-status-online); } @@ -392,23 +355,11 @@ i.status-busy { color: var(--rc-status-busy); } -.status-bg-busy { - background-color: var(--rc-status-busy); -} - .popup-user-status-busy, .status-busy::after { background-color: var(--rc-status-busy); } -i.status-offline { - color: var(--rc-status-offline); -} - -.status-bg-offline { - background-color: var(--rc-status-offline); -} - .popup-user-status-offline, .status-offline::after { background-color: var(--rc-status-offline); diff --git a/apps/meteor/app/theme/client/main.css b/apps/meteor/app/theme/client/main.css index 1795e14bbef..7d71e8a6e0f 100644 --- a/apps/meteor/app/theme/client/main.css +++ b/apps/meteor/app/theme/client/main.css @@ -12,7 +12,6 @@ @import 'imports/forms/input.css'; @import 'imports/forms/select.css'; @import 'imports/forms/popup-list.css'; -@import 'imports/forms/select-avatar.css'; @import 'imports/forms/switch.css'; @import 'imports/forms/tags.css'; @import 'imports/forms/checkbox.css'; @@ -22,7 +21,6 @@ /* Sidebar */ @import 'imports/components/sidebar/sidebar.css'; -@import 'imports/components/sidebar/sidebar-flex.css'; @import 'imports/components/sidebar/rooms-list.css'; /* Main */ @@ -31,20 +29,14 @@ @import 'imports/components/main-content.css'; @import 'imports/components/message-box.css'; @import 'imports/components/avatar.css'; -@import 'imports/components/badge.css'; @import 'imports/components/popover.css'; @import 'imports/components/popout.css'; @import 'imports/components/modal.css'; -@import 'imports/components/chip.css'; @import 'imports/components/messages.css'; @import 'imports/components/read-receipts.css'; @import 'imports/components/contextual-bar.css'; @import 'imports/components/emojiPicker.css'; @import 'imports/components/table.css'; -@import 'imports/components/tabs.css'; - -/* Modal */ -@import 'imports/components/modal/create-channel.css'; /* User Info */ @import 'imports/components/userInfo.css'; diff --git a/apps/meteor/app/ui/client/components/tabs.html b/apps/meteor/app/ui/client/components/tabs.html deleted file mode 100644 index 5ed7ec7395d..00000000000 --- a/apps/meteor/app/ui/client/components/tabs.html +++ /dev/null @@ -1,16 +0,0 @@ -<template name="tabs"> - <div class="tabs"> - <div class="tabs-wrapper" role="tablist"> - {{#each tab in tabs}} - <button - class="tab {{#if isActive tab.value}}active{{/if}}" - data-value={{tab.value}} - {{ariaSelected tab.value}} - role="tab" - > - <span tabindex="-1">{{tab.label}}</span> - </button> - {{/each}} - </div> - </div> -</template> diff --git a/apps/meteor/app/ui/client/components/tabs.js b/apps/meteor/app/ui/client/components/tabs.js deleted file mode 100644 index 6e5c0c81590..00000000000 --- a/apps/meteor/app/ui/client/components/tabs.js +++ /dev/null @@ -1,31 +0,0 @@ -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import './tabs.html'; - -Template.tabs.onCreated(function () { - this.activeTab = new ReactiveVar(this.data.tabs.tabs.find((tab) => tab.active).value); -}); - -Template.tabs.events({ - 'click .tab'(event, instance) { - const { value } = event.currentTarget.dataset; - if (value === instance.activeTab.get()) { - return; - } - instance.activeTab.set(value); - instance.data.tabs.onChange(value); - }, -}); - -Template.tabs.helpers({ - tabs() { - return Template.instance().data.tabs.tabs.filter((tab) => (tab.condition ? tab.condition() : tab)); - }, - isActive(value) { - return Template.instance().activeTab.get() === value; - }, - ariaSelected(value) { - return Template.instance().activeTab.get() === value ? { 'aria-selected': 'true' } : {}; - }, -}); diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index 4e81639b856..18deb29f36f 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -12,12 +12,11 @@ import './views/app/photoswipeContent.ts'; // without the *.ts extension, *.html import './components/icon'; import './components/table.html'; import './components/table'; -import './components/tabs'; import './components/popupList.html'; import './components/popupList'; import './components/selectDropdown.html'; -import './components/tooltip'; +// import './components/tooltip'; export { ChatMessages, chatMessages } from './lib/ChatMessages'; export { fileUpload } from './lib/fileUpload'; -- GitLab From 5f7d966c22586db932e63e31952342315d28e03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <henrique.jobs1@gmail.com> Date: Fri, 9 Sep 2022 16:39:28 -0300 Subject: [PATCH 020/107] [NEW] Sections layout and featured apps for marketplace (#26514) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .../app/apps/server/communication/rest.js | 4 +- apps/meteor/client/components/Page/Page.tsx | 15 +- .../client/views/admin/apps/AppDetails.tsx | 10 +- .../views/admin/apps/AppDetailsPage.tsx | 2 +- .../client/views/admin/apps/AppStatus.js | 2 +- .../client/views/admin/apps/AppsList.tsx | 216 ++---------------- .../client/views/admin/apps/AppsListMain.tsx | 68 ------ .../client/views/admin/apps/AppsPage.tsx | 13 +- .../views/admin/apps/AppsPageContent.tsx | 142 ++++++++++++ .../admin/apps/AppsPageContentSkeleton.tsx | 22 ++ .../client/views/admin/apps/BundleChips.tsx | 8 +- .../admin/apps/ConnectionErrorEmptyState.tsx | 25 ++ .../views/admin/apps/FeaturedAppsSections.tsx | 36 +++ .../apps/NoInstalledAppMatchesEmptyState.tsx | 51 +++++ .../apps/NoInstalledAppsFoundEmptyState.tsx | 22 ++ ...etplaceOrInstalledAppMatchesEmptyState.tsx | 47 ++++ .../admin/apps/components/RadioButtonList.tsx | 4 +- .../RadioDropDown/RadioDropDown.tsx | 4 +- .../apps/helpers/normalizeFeaturedApps.ts | 10 + .../views/admin/apps/hooks/useFeaturedApps.ts | 9 + .../views/admin/settings/SettingsPage.tsx | 2 +- .../client/views/home/DefaultHomePage.tsx | 2 +- .../EngagementDashboardPage.tsx | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 12 + packages/core-typings/src/AppOverview.ts | 115 ++++++++++ packages/core-typings/src/Apps.ts | 2 +- packages/core-typings/src/FeaturedApps.ts | 8 +- packages/core-typings/src/index.ts | 1 + packages/rest-typings/src/apps/index.ts | 6 +- 29 files changed, 557 insertions(+), 303 deletions(-) delete mode 100644 apps/meteor/client/views/admin/apps/AppsListMain.tsx create mode 100644 apps/meteor/client/views/admin/apps/AppsPageContent.tsx create mode 100644 apps/meteor/client/views/admin/apps/AppsPageContentSkeleton.tsx create mode 100644 apps/meteor/client/views/admin/apps/ConnectionErrorEmptyState.tsx create mode 100644 apps/meteor/client/views/admin/apps/FeaturedAppsSections.tsx create mode 100644 apps/meteor/client/views/admin/apps/NoInstalledAppMatchesEmptyState.tsx create mode 100644 apps/meteor/client/views/admin/apps/NoInstalledAppsFoundEmptyState.tsx create mode 100644 apps/meteor/client/views/admin/apps/NoMarketplaceOrInstalledAppMatchesEmptyState.tsx create mode 100644 apps/meteor/client/views/admin/apps/helpers/normalizeFeaturedApps.ts create mode 100644 apps/meteor/client/views/admin/apps/hooks/useFeaturedApps.ts create mode 100644 packages/core-typings/src/AppOverview.ts diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 79276a15a8b..7fa99248b41 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -348,7 +348,7 @@ export class AppsRestApi { ); this.api.addRoute( - 'featured', + 'featured-apps', { authRequired: true }, { async get() { @@ -362,7 +362,7 @@ export class AppsRestApi { let result; try { - result = HTTP.get(`${baseUrl}/v1/apps/featured`, { + result = HTTP.get(`${baseUrl}/v1/featured-apps`, { headers, }); } catch (e) { diff --git a/apps/meteor/client/components/Page/Page.tsx b/apps/meteor/client/components/Page/Page.tsx index 801c4bce89f..57a4c294ad4 100644 --- a/apps/meteor/client/components/Page/Page.tsx +++ b/apps/meteor/client/components/Page/Page.tsx @@ -1,14 +1,24 @@ import { Box } from '@rocket.chat/fuselage'; +import Colors from '@rocket.chat/fuselage-tokens/colors'; import React, { useState, ReactElement, ComponentProps } from 'react'; import PageContext from './PageContext'; -const Page = (props: ComponentProps<typeof Box>): ReactElement => { +type PageProps = Omit<ComponentProps<typeof Box>, 'backgroundColor'> & { + background?: 'light' | 'tint'; +}; + +const surfaceMap = { + light: Colors.white, + tint: Colors.n100, + neutral: Colors.n400, +}; // TODO: Remove this export after the migration is complete + +const Page = ({ background = 'light', ...props }: PageProps): ReactElement => { const [border, setBorder] = useState(false); return ( <PageContext.Provider value={[border, setBorder]}> <Box - backgroundColor='surface' is='section' display='flex' flexDirection='column' @@ -17,6 +27,7 @@ const Page = (props: ComponentProps<typeof Box>): ReactElement => { height='full' overflow='hidden' {...props} + backgroundColor={`var(--rcx-color-surface-${background}, ${surfaceMap[background]})`} /> </PageContext.Provider> ); diff --git a/apps/meteor/client/views/admin/apps/AppDetails.tsx b/apps/meteor/client/views/admin/apps/AppDetails.tsx index 4cdeac6939d..26a72f67f00 100644 --- a/apps/meteor/client/views/admin/apps/AppDetails.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetails.tsx @@ -1,4 +1,4 @@ -import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage'; +import { Box, ButtonGroup, Callout, Chip, Margins } from '@rocket.chat/fuselage'; import { ExternalLink } from '@rocket.chat/ui-client'; import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; @@ -59,13 +59,13 @@ const AppDetails: FC<AppDetailsProps> = ({ app }) => { <Box fontScale='h4' mbe='x8'> {t('Categories')} </Box> - <Box display='flex' flexDirection='row'> + <ButtonGroup medium flexWrap='wrap'> {categories?.map((current) => ( - <Chip key={current} textTransform='uppercase' mie='x8'> - <Box color='hint'>{current}</Box> + <Chip key={current} textTransform='uppercase'> + {current} </Chip> ))} - </Box> + </ButtonGroup> </Box> <Box is='section'> diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage.tsx index 569a5f7da58..1eee7d19934 100644 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage.tsx @@ -86,7 +86,7 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) { <> <AppDetailsHeader app={appData} /> - <Tabs mis='-x24' mb='x36'> + <Tabs> <Tabs.Item onClick={(): void => handleTabClick('details')} selected={!tab || tab === 'details'}> {t('Details')} </Tabs.Item> diff --git a/apps/meteor/client/views/admin/apps/AppStatus.js b/apps/meteor/client/views/admin/apps/AppStatus.js index 1daf225261c..41bffb19faf 100644 --- a/apps/meteor/client/views/admin/apps/AppStatus.js +++ b/apps/meteor/client/views/admin/apps/AppStatus.js @@ -124,7 +124,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, isSubscribed, ins flexDirection='row' alignItems='center' justifyContent='center' - borderRadius='x2' + borderRadius='x4' invisible={!showStatus && !loading} > <Button diff --git a/apps/meteor/client/views/admin/apps/AppsList.tsx b/apps/meteor/client/views/admin/apps/AppsList.tsx index a3c96bbdf38..00dda95a4af 100644 --- a/apps/meteor/client/views/admin/apps/AppsList.tsx +++ b/apps/meteor/client/views/admin/apps/AppsList.tsx @@ -1,202 +1,26 @@ -import { - Box, - States, - StatesAction, - StatesActions, - StatesIcon, - StatesSubtitle, - StatesSuggestion, - StatesSuggestionList, - StatesSuggestionListItem, - StatesSuggestionText, - StatesTitle, - Icon, -} from '@rocket.chat/fuselage'; -import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { FC, useMemo, useState } from 'react'; +import { App } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; -import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; -import { AsyncStatePhase } from '../../../lib/asyncState'; -import { useAppsReload, useAppsResult } from './AppsContext'; -import AppsFilters from './AppsFilters'; -import AppsListMain from './AppsListMain'; -import { RadioDropDownGroup } from './definitions/RadioDropDownDefinitions'; -import { useCategories } from './hooks/useCategories'; -import { useFilteredApps } from './hooks/useFilteredApps'; -import { useRadioToggle } from './hooks/useRadioToggle'; +import AppRow from './AppRow'; -const AppsList: FC<{ +type AppsListProps = { + apps: App[]; + title: string; isMarketplace: boolean; -}> = ({ isMarketplace }) => { - const t = useTranslation(); - const { marketplaceApps, installedApps } = useAppsResult(); - const [text, setText] = useDebouncedState('', 500); - const reload = useAppsReload(); - const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); - - const marketplaceRoute = useRoute('admin-marketplace'); - - const [freePaidFilterStructure, setFreePaidFilterStructure] = useState({ - label: t('Filter_By_Price'), - items: [ - { id: 'all', label: t('All_Prices'), checked: true }, - { id: 'free', label: t('Free_Apps'), checked: false }, - { id: 'paid', label: t('Paid_Apps'), checked: false }, - ], - }); - const freePaidFilterOnSelected = useRadioToggle(setFreePaidFilterStructure); - - const [statusFilterStructure, setStatusFilterStructure] = useState({ - label: t('Filter_By_Status'), - items: [ - { id: 'all', label: t('All_status'), checked: true }, - { id: 'enabled', label: t('Enabled'), checked: false }, - { id: 'disabled', label: t('Disabled'), checked: false }, - ], - }); - const statusFilterOnSelected = useRadioToggle(setStatusFilterStructure); - - const [sortFilterStructure, setSortFilterStructure] = useState<RadioDropDownGroup>({ - label: t('Sort_By'), - items: [ - { id: 'az', label: 'A-Z', checked: true }, - { id: 'za', label: 'Z-A', checked: false }, - { id: 'mru', label: t('Most_recent_updated'), checked: false }, - { id: 'lru', label: t('Least_recent_updated'), checked: false }, - ], - }); - const sortFilterOnSelected = useRadioToggle(setSortFilterStructure); - - const [categories, selectedCategories, categoryTagList, onSelected] = useCategories(); - const appsResult = useFilteredApps({ - appsData: isMarketplace ? marketplaceApps : installedApps, - text, - current, - itemsPerPage, - categories: useMemo(() => selectedCategories.map(({ label }) => label), [selectedCategories]), - purchaseType: useMemo(() => freePaidFilterStructure.items.find(({ checked }) => checked)?.id, [freePaidFilterStructure]), - sortingMethod: useMemo(() => sortFilterStructure.items.find(({ checked }) => checked)?.id, [sortFilterStructure]), - status: useMemo(() => statusFilterStructure.items.find(({ checked }) => checked)?.id, [statusFilterStructure]), - }); - - const isAppListReadyOrLoading = - appsResult.phase === AsyncStatePhase.LOADING || (appsResult.phase === AsyncStatePhase.RESOLVED && Boolean(appsResult.value.count)); - - const noInstalledAppsFound = appsResult.phase === AsyncStatePhase.RESOLVED && !isMarketplace && appsResult.value.total === 0; - - const noMarketplaceOrInstalledAppMatches = appsResult.phase === AsyncStatePhase.RESOLVED && isMarketplace && appsResult.value.count === 0; - - const noInstalledAppMatches = - appsResult.phase === AsyncStatePhase.RESOLVED && !isMarketplace && appsResult.value.total !== 0 && appsResult.value.count === 0; - - return ( - <> - <AppsFilters - setText={setText} - freePaidFilterStructure={freePaidFilterStructure} - freePaidFilterOnSelected={freePaidFilterOnSelected} - categories={categories} - selectedCategories={selectedCategories} - onSelected={onSelected} - sortFilterStructure={sortFilterStructure} - sortFilterOnSelected={sortFilterOnSelected} - categoryTagList={categoryTagList} - statusFilterStructure={statusFilterStructure} - statusFilterOnSelected={statusFilterOnSelected} - /> - - {isAppListReadyOrLoading && ( - <AppsListMain - appsResult={appsResult} - current={current} - itemsPerPage={itemsPerPage} - onSetItemsPerPage={onSetItemsPerPage} - onSetCurrent={onSetCurrent} - paginationProps={paginationProps} - isMarketplace={isMarketplace} - /> - )} - - {noMarketplaceOrInstalledAppMatches && ( - <Box mbs='x20'> - <States> - <StatesIcon name='magnifier' /> - <StatesTitle>{t('No_app_matches')}</StatesTitle> - {appsResult?.value?.shouldShowSearchText ? ( - <StatesSubtitle> - {t('No_marketplace_matches_for')}: <strong>"{text}"</strong> - </StatesSubtitle> - ) : ( - '' - )} - <StatesSuggestion> - <StatesSuggestionText>{t('You_can_try_to')}:</StatesSuggestionText> - <StatesSuggestionList> - <StatesSuggestionListItem>{t('Search_by_category')}</StatesSuggestionListItem> - <StatesSuggestionListItem>{t('Search_for_a_more_general_term')}</StatesSuggestionListItem> - <StatesSuggestionListItem>{t('Search_for_a_more_specific_term')}</StatesSuggestionListItem> - <StatesSuggestionListItem>{t('Check_if_the_spelling_is_correct')}</StatesSuggestionListItem> - </StatesSuggestionList> - </StatesSuggestion> - </States> - </Box> - )} - - {noInstalledAppMatches && ( - <Box mbs='x20'> - <States> - <StatesIcon name='magnifier' /> - <StatesTitle>{t('No_installed_app_matches')}</StatesTitle> - {appsResult?.value?.shouldShowSearchText ? ( - <StatesSubtitle> - <span> - {t('No_app_matches_for')} <strong>"{text}"</strong> - </span> - </StatesSubtitle> - ) : ( - '' - )} - <StatesSuggestion> - <StatesSuggestionText>{t('Try_searching_in_the_marketplace_instead')}</StatesSuggestionText> - </StatesSuggestion> - <StatesActions> - <StatesAction onClick={(): void => marketplaceRoute.push({ context: '' })}>{t('Search_on_marketplace')}</StatesAction> - </StatesActions> - </States> - </Box> - )} - - {noInstalledAppsFound && ( - <Box mbs='x20'> - <States> - <StatesIcon name='magnifier' /> - <StatesTitle>{t('No_apps_installed')}</StatesTitle> - <StatesSubtitle>{t('Explore_the_marketplace_to_find_awesome_apps')}</StatesSubtitle> - <StatesActions> - <StatesAction onClick={(): void => marketplaceRoute.push({ context: '' })}>{t('Explore_marketplace')}</StatesAction> - </StatesActions> - </States> - </Box> - )} - - {appsResult.phase === AsyncStatePhase.REJECTED && ( - <Box mbs='x20'> - <States> - <StatesIcon variation='danger' name='circle-exclamation' /> - <StatesTitle>{t('Connection_error')}</StatesTitle> - <StatesSubtitle>{t('Marketplace_error')}</StatesSubtitle> - <StatesActions> - <StatesAction onClick={reload}> - <Icon mie='x4' size='x20' name='reload' /> - {t('Reload_page')} - </StatesAction> - </StatesActions> - </States> - </Box> - )} - </> - ); }; +const AppsList = ({ apps, title, isMarketplace }: AppsListProps): ReactElement => ( + <> + <Box is='h3' fontScale='h3' color='default' mbe='x20'> + {title} + </Box> + <Box mbe='x36'> + {apps.map((app) => ( + <AppRow key={app.id} isMarketplace={isMarketplace} {...app} /> + ))} + </Box> + </> +); + export default AppsList; diff --git a/apps/meteor/client/views/admin/apps/AppsListMain.tsx b/apps/meteor/client/views/admin/apps/AppsListMain.tsx deleted file mode 100644 index ddb9b67f7a3..00000000000 --- a/apps/meteor/client/views/admin/apps/AppsListMain.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { App } from '@rocket.chat/core-typings'; -import { Box, Pagination, Skeleton } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; -import React, { ReactElement } from 'react'; - -import { AsyncState, AsyncStatePhase } from '../../../lib/asyncState'; -import AppRow from './AppRow'; - -type itemsPerPage = 25 | 50 | 100; - -type AppsListMainProps = { - appsResult: AsyncState< - { - items: App[]; - } & { - shouldShowSearchText: boolean; - } & { - count: number; - offset: number; - total: number; - } - >; - current: number; - itemsPerPage: itemsPerPage; - onSetItemsPerPage: React.Dispatch<React.SetStateAction<itemsPerPage>>; - onSetCurrent: React.Dispatch<React.SetStateAction<number>>; - paginationProps: { - itemsPerPageLabel: () => string; - showingResultsLabel: (context: { count: number; current: number; itemsPerPage: itemsPerPage }) => string; - }; - isMarketplace: boolean; -}; - -const AppsListMain = ({ - appsResult, - current, - itemsPerPage, - onSetItemsPerPage, - onSetCurrent, - paginationProps, - isMarketplace, -}: AppsListMainProps): ReactElement => { - const loadingRows = Array.from({ length: 8 }, (_, i) => <Skeleton key={i} height='x56' mbe='x8' width='100%' variant='rect' />); - - return ( - <> - <Box overflowY='auto' height='100%'> - {appsResult.phase === AsyncStatePhase.LOADING - ? loadingRows - : appsResult.phase === AsyncStatePhase.RESOLVED && - appsResult.value.items.map((app) => <AppRow key={app.id} isMarketplace={isMarketplace} {...app} />)} - </Box> - {appsResult.phase === AsyncStatePhase.RESOLVED && ( - <Pagination - current={current} - itemsPerPage={itemsPerPage} - count={appsResult.value.total} - onSetItemsPerPage={onSetItemsPerPage} - onSetCurrent={onSetCurrent} - borderBlockStart={`2px solid ${colors.n300}`} - {...paginationProps} - /> - )} - </> - ); -}; - -export default AppsListMain; diff --git a/apps/meteor/client/views/admin/apps/AppsPage.tsx b/apps/meteor/client/views/admin/apps/AppsPage.tsx index 71fb447282e..ac3e221101c 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage.tsx @@ -1,10 +1,9 @@ import { Button, ButtonGroup, Icon, Skeleton, Tabs } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; import { useRoute, useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, ReactElement } from 'react'; import Page from '../../../components/Page'; -import AppsList from './AppsList'; +import AppsPageContent from './AppsPageContent'; type AppsPageProps = { isMarketplace: boolean; @@ -37,8 +36,8 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { }; return ( - <Page> - <Page.Header title={t('Apps')} bg={colors.n100}> + <Page background='tint'> + <Page.Header title={t('Apps')}> <ButtonGroup> {isMarketplace && !isLoggedInCloud && ( <Button disabled={isLoggedInCloud === undefined} onClick={handleLoginButtonClick}> @@ -58,7 +57,7 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { )} </ButtonGroup> </Page.Header> - <Tabs bg={colors.n100}> + <Tabs> <Tabs.Item onClick={(): void => marketplaceRoute.push({ context: '' })} selected={isMarketplace}> {t('Marketplace')} </Tabs.Item> @@ -66,8 +65,8 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { {t('Installed')} </Tabs.Item> </Tabs> - <Page.Content bg={colors.n100}> - <AppsList isMarketplace={isMarketplace} /> + <Page.Content overflowY='auto'> + <AppsPageContent isMarketplace={isMarketplace} /> </Page.Content> </Page> ); diff --git a/apps/meteor/client/views/admin/apps/AppsPageContent.tsx b/apps/meteor/client/views/admin/apps/AppsPageContent.tsx new file mode 100644 index 00000000000..422916a24be --- /dev/null +++ b/apps/meteor/client/views/admin/apps/AppsPageContent.tsx @@ -0,0 +1,142 @@ +import { Pagination, Divider } from '@rocket.chat/fuselage'; +import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; +import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useMemo, useState } from 'react'; + +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { AsyncStatePhase } from '../../../lib/asyncState'; +import { useAppsReload, useAppsResult } from './AppsContext'; +import AppsFilters from './AppsFilters'; +import AppsList from './AppsList'; +import AppsPageContentSkeleton from './AppsPageContentSkeleton'; +import ConnectionErrorEmptyState from './ConnectionErrorEmptyState'; +import FeaturedAppsSections from './FeaturedAppsSections'; +import NoInstalledAppMatchesEmptyState from './NoInstalledAppMatchesEmptyState'; +import NoInstalledAppsFoundEmptyState from './NoInstalledAppsFoundEmptyState'; +import NoMarketplaceOrInstalledAppMatchesEmptyState from './NoMarketplaceOrInstalledAppMatchesEmptyState'; +import { RadioDropDownGroup } from './definitions/RadioDropDownDefinitions'; +import { useCategories } from './hooks/useCategories'; +import { useFilteredApps } from './hooks/useFilteredApps'; +import { useRadioToggle } from './hooks/useRadioToggle'; + +const AppsPageContent = ({ isMarketplace }: { isMarketplace: boolean }): ReactElement => { + const t = useTranslation(); + const { marketplaceApps, installedApps } = useAppsResult(); + const [text, setText] = useDebouncedState('', 500); + const reload = useAppsReload(); + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const marketplaceRoute = useRoute('admin-marketplace'); + + const [freePaidFilterStructure, setFreePaidFilterStructure] = useState({ + label: t('Filter_By_Price'), + items: [ + { id: 'all', label: t('All_Prices'), checked: true }, + { id: 'free', label: t('Free_Apps'), checked: false }, + { id: 'paid', label: t('Paid_Apps'), checked: false }, + ], + }); + const freePaidFilterOnSelected = useRadioToggle(setFreePaidFilterStructure); + + const [statusFilterStructure, setStatusFilterStructure] = useState({ + label: t('Filter_By_Status'), + items: [ + { id: 'all', label: t('All_status'), checked: true }, + { id: 'enabled', label: t('Enabled'), checked: false }, + { id: 'disabled', label: t('Disabled'), checked: false }, + ], + }); + const statusFilterOnSelected = useRadioToggle(setStatusFilterStructure); + + const [sortFilterStructure, setSortFilterStructure] = useState<RadioDropDownGroup>({ + label: t('Sort_By'), + items: [ + { id: 'mru', label: t('Most_recent_updated'), checked: true }, + { id: 'lru', label: t('Least_recent_updated'), checked: false }, + { id: 'az', label: 'A-Z', checked: false }, + { id: 'za', label: 'Z-A', checked: false }, + ], + }); + const sortFilterOnSelected = useRadioToggle(setSortFilterStructure); + + const [categories, selectedCategories, categoryTagList, onSelected] = useCategories(); + const appsResult = useFilteredApps({ + appsData: isMarketplace ? marketplaceApps : installedApps, + text, + current, + itemsPerPage, + categories: useMemo(() => selectedCategories.map(({ label }) => label), [selectedCategories]), + purchaseType: useMemo(() => freePaidFilterStructure.items.find(({ checked }) => checked)?.id, [freePaidFilterStructure]), + sortingMethod: useMemo(() => sortFilterStructure.items.find(({ checked }) => checked)?.id, [sortFilterStructure]), + status: useMemo(() => statusFilterStructure.items.find(({ checked }) => checked)?.id, [statusFilterStructure]), + }); + + const noInstalledAppsFound = appsResult.phase === AsyncStatePhase.RESOLVED && !isMarketplace && appsResult.value.total === 0; + + const noMarketplaceOrInstalledAppMatches = appsResult.phase === AsyncStatePhase.RESOLVED && isMarketplace && appsResult.value.count === 0; + + const noInstalledAppMatches = + appsResult.phase === AsyncStatePhase.RESOLVED && !isMarketplace && appsResult.value.total !== 0 && appsResult.value.count === 0; + + const isFiltered = + Boolean(text.length) || + freePaidFilterStructure.items.find((item) => item.checked)?.id !== 'all' || + statusFilterStructure.items.find((item) => item.checked)?.id !== 'all' || + sortFilterStructure.items.find((item) => item.checked)?.id !== 'mru' || + selectedCategories.length > 0; + + return ( + <> + <AppsFilters + setText={setText} + freePaidFilterStructure={freePaidFilterStructure} + freePaidFilterOnSelected={freePaidFilterOnSelected} + categories={categories} + selectedCategories={selectedCategories} + onSelected={onSelected} + sortFilterStructure={sortFilterStructure} + sortFilterOnSelected={sortFilterOnSelected} + categoryTagList={categoryTagList} + statusFilterStructure={statusFilterStructure} + statusFilterOnSelected={statusFilterOnSelected} + /> + {appsResult.phase === AsyncStatePhase.LOADING && <AppsPageContentSkeleton />} + + {appsResult.phase === AsyncStatePhase.RESOLVED && + !noMarketplaceOrInstalledAppMatches && + (!noInstalledAppMatches || !noInstalledAppsFound) && ( + <> + {isMarketplace && !isFiltered && <FeaturedAppsSections appsResult={appsResult.value.items} />} + {!noInstalledAppsFound && <AppsList apps={appsResult.value.items} title={t('All_Apps')} isMarketplace={isMarketplace} />} + {Boolean(appsResult.value.count) && ( + <> + <Divider /> + <Pagination + current={current} + itemsPerPage={itemsPerPage} + count={appsResult.value.total} + onSetItemsPerPage={onSetItemsPerPage} + onSetCurrent={onSetCurrent} + {...paginationProps} + /> + </> + )} + </> + )} + {noMarketplaceOrInstalledAppMatches && ( + <NoMarketplaceOrInstalledAppMatchesEmptyState shouldShowSearchText={appsResult.value.shouldShowSearchText} text={text} /> + )} + {noInstalledAppMatches && ( + <NoInstalledAppMatchesEmptyState + shouldShowSearchText={appsResult.value.shouldShowSearchText} + text={text} + onButtonClick={(): void => marketplaceRoute.push({ context: '' })} + /> + )} + {noInstalledAppsFound && <NoInstalledAppsFoundEmptyState onButtonClick={(): void => marketplaceRoute.push({ context: '' })} />} + {appsResult.phase === AsyncStatePhase.REJECTED && <ConnectionErrorEmptyState onButtonClick={reload} />} + </> + ); +}; + +export default AppsPageContent; diff --git a/apps/meteor/client/views/admin/apps/AppsPageContentSkeleton.tsx b/apps/meteor/client/views/admin/apps/AppsPageContentSkeleton.tsx new file mode 100644 index 00000000000..bbd21947f71 --- /dev/null +++ b/apps/meteor/client/views/admin/apps/AppsPageContentSkeleton.tsx @@ -0,0 +1,22 @@ +import { Skeleton, Box } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +const AppsPageContentSkeleton = (): ReactElement => { + const loadingRows = Array.from({ length: 3 }, (_, i) => <Skeleton key={i} height='x56' mbe='x8' width='100%' variant='rect' />); + return ( + <> + <Box mbe='x36'> + <Skeleton height='x28' width='x150' mbe='x20' variant='rect' /> + {loadingRows} + </Box> + <Box mbe='x36'> + <Skeleton height='x28' width='x150' mbe='x20' variant='rect' /> + {loadingRows} + </Box> + <Skeleton height='x28' width='x150' mbe='x20' variant='rect' /> + {loadingRows} + </> + ); +}; + +export default AppsPageContentSkeleton; diff --git a/apps/meteor/client/views/admin/apps/BundleChips.tsx b/apps/meteor/client/views/admin/apps/BundleChips.tsx index c5dd73376f0..73aabdb4293 100644 --- a/apps/meteor/client/views/admin/apps/BundleChips.tsx +++ b/apps/meteor/client/views/admin/apps/BundleChips.tsx @@ -1,6 +1,6 @@ import { Box, Icon, PositionAnimated, AnimatedVisibility, Tooltip } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { RefObject, useRef, useState, ReactElement } from 'react'; +import React, { RefObject, useRef, useState, ReactElement, Fragment } from 'react'; import { App } from './types'; @@ -22,7 +22,7 @@ const BundleChips = ({ bundledIn, isIconOnly }: BundleChipsProps): ReactElement return ( <> {bundledIn.map((bundle) => ( - <> + <Fragment key={bundle.bundleId}> <Box display='flex' flexDirection='row' @@ -38,7 +38,7 @@ const BundleChips = ({ bundledIn, isIconOnly }: BundleChipsProps): ReactElement > <Icon name='bag' size='x20' /> {!isIconOnly && ( - <Box fontWeight='700' fontSize='x12' color='info' style={{ whiteSpace: 'nowrap' }}> + <Box fontScale='c2' color='info' style={{ whiteSpace: 'nowrap' }}> {t('bundle_chip_title', { bundleName: bundle.bundleName, })} @@ -57,7 +57,7 @@ const BundleChips = ({ bundledIn, isIconOnly }: BundleChipsProps): ReactElement })} </Tooltip> </PositionAnimated> - </> + </Fragment> ))} </> ); diff --git a/apps/meteor/client/views/admin/apps/ConnectionErrorEmptyState.tsx b/apps/meteor/client/views/admin/apps/ConnectionErrorEmptyState.tsx new file mode 100644 index 00000000000..2db606117f2 --- /dev/null +++ b/apps/meteor/client/views/admin/apps/ConnectionErrorEmptyState.tsx @@ -0,0 +1,25 @@ +import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesAction, Icon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +const ConnectionErrorEmptyState = ({ onButtonClick }: { onButtonClick: () => void }): ReactElement => { + const t = useTranslation(); + + return ( + <Box mbs='x20'> + <States> + <StatesIcon variation='danger' name='circle-exclamation' /> + <StatesTitle>{t('Connection_error')}</StatesTitle> + <StatesSubtitle>{t('Marketplace_error')}</StatesSubtitle> + <StatesActions> + <StatesAction onClick={onButtonClick}> + <Icon mie='x4' size='x20' name='reload' /> + {t('Reload_page')} + </StatesAction> + </StatesActions> + </States> + </Box> + ); +}; + +export default ConnectionErrorEmptyState; diff --git a/apps/meteor/client/views/admin/apps/FeaturedAppsSections.tsx b/apps/meteor/client/views/admin/apps/FeaturedAppsSections.tsx new file mode 100644 index 00000000000..faab9cf91fd --- /dev/null +++ b/apps/meteor/client/views/admin/apps/FeaturedAppsSections.tsx @@ -0,0 +1,36 @@ +import { App } from '@rocket.chat/core-typings'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import AppsList from './AppsList'; +import normalizeFeaturedApps from './helpers/normalizeFeaturedApps'; +import { useFeaturedApps } from './hooks/useFeaturedApps'; + +type FeaturedSectionsProps = { + appsResult: App[]; +}; + +const FeaturedAppsSections = ({ appsResult }: FeaturedSectionsProps): ReactElement | null => { + const t = useTranslation(); + + const featuredApps = useFeaturedApps(); + + if (featuredApps.isSuccess) { + return ( + <> + {featuredApps.data.sections.map((section) => ( + <AppsList + key={section.slug} + apps={normalizeFeaturedApps(section.apps, appsResult)} + title={t.has(section.i18nLabel) ? t(section.i18nLabel) : section.i18nLabel} + isMarketplace={true} + /> + ))} + </> + ); + } + + return null; +}; + +export default FeaturedAppsSections; diff --git a/apps/meteor/client/views/admin/apps/NoInstalledAppMatchesEmptyState.tsx b/apps/meteor/client/views/admin/apps/NoInstalledAppMatchesEmptyState.tsx new file mode 100644 index 00000000000..8dfac474b77 --- /dev/null +++ b/apps/meteor/client/views/admin/apps/NoInstalledAppMatchesEmptyState.tsx @@ -0,0 +1,51 @@ +import { + Box, + States, + StatesIcon, + StatesTitle, + StatesSubtitle, + StatesSuggestion, + StatesSuggestionText, + StatesActions, + StatesAction, +} from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +type NoInstalledAppMatchesEmptyStateProps = { + shouldShowSearchText: boolean; + text: string; + onButtonClick: () => void; +}; + +const NoInstalledAppMatchesEmptyState = ({ + shouldShowSearchText, + text, + onButtonClick, +}: NoInstalledAppMatchesEmptyStateProps): ReactElement => { + const t = useTranslation(); + + return ( + <Box mbs='x20'> + <States> + <StatesIcon name='magnifier' /> + <StatesTitle>{t('No_installed_app_matches')}</StatesTitle> + {shouldShowSearchText && ( + <StatesSubtitle> + <span> + {t('No_app_matches_for')} <strong>"{text}"</strong> + </span> + </StatesSubtitle> + )} + <StatesSuggestion> + <StatesSuggestionText>{t('Try_searching_in_the_marketplace_instead')}</StatesSuggestionText> + </StatesSuggestion> + <StatesActions> + <StatesAction onClick={onButtonClick}>{t('Search_on_marketplace')}</StatesAction> + </StatesActions> + </States> + </Box> + ); +}; + +export default NoInstalledAppMatchesEmptyState; diff --git a/apps/meteor/client/views/admin/apps/NoInstalledAppsFoundEmptyState.tsx b/apps/meteor/client/views/admin/apps/NoInstalledAppsFoundEmptyState.tsx new file mode 100644 index 00000000000..1977c8f149c --- /dev/null +++ b/apps/meteor/client/views/admin/apps/NoInstalledAppsFoundEmptyState.tsx @@ -0,0 +1,22 @@ +import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +const NoInstalledAppsFoundEmptyState = ({ onButtonClick }: { onButtonClick: () => void }): ReactElement => { + const t = useTranslation(); + + return ( + <Box mbs='x20'> + <States> + <StatesIcon name='magnifier' /> + <StatesTitle>{t('No_apps_installed')}</StatesTitle> + <StatesSubtitle>{t('Explore_the_marketplace_to_find_awesome_apps')}</StatesSubtitle> + <StatesActions> + <StatesAction onClick={onButtonClick}>{t('Explore_marketplace')}</StatesAction> + </StatesActions> + </States> + </Box> + ); +}; + +export default NoInstalledAppsFoundEmptyState; diff --git a/apps/meteor/client/views/admin/apps/NoMarketplaceOrInstalledAppMatchesEmptyState.tsx b/apps/meteor/client/views/admin/apps/NoMarketplaceOrInstalledAppMatchesEmptyState.tsx new file mode 100644 index 00000000000..eb2bdcb10dc --- /dev/null +++ b/apps/meteor/client/views/admin/apps/NoMarketplaceOrInstalledAppMatchesEmptyState.tsx @@ -0,0 +1,47 @@ +import { + Box, + States, + StatesIcon, + StatesSubtitle, + StatesSuggestion, + StatesSuggestionList, + StatesSuggestionListItem, + StatesSuggestionText, + StatesTitle, +} from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +type NoMarketplaceOrInstalledAppMatchesEmptyStateProps = { shouldShowSearchText: boolean; text: string }; + +const NoMarketplaceOrInstalledAppMatchesEmptyState = ({ + shouldShowSearchText, + text, +}: NoMarketplaceOrInstalledAppMatchesEmptyStateProps): ReactElement => { + const t = useTranslation(); + + return ( + <Box mbs='x20'> + <States> + <StatesIcon name='magnifier' /> + <StatesTitle>{t('No_app_matches')}</StatesTitle> + {shouldShowSearchText && ( + <StatesSubtitle> + {t('No_marketplace_matches_for')}: <strong>"{text}"</strong> + </StatesSubtitle> + )} + <StatesSuggestion> + <StatesSuggestionText>{t('You_can_try_to')}:</StatesSuggestionText> + <StatesSuggestionList> + <StatesSuggestionListItem>{t('Search_by_category')}</StatesSuggestionListItem> + <StatesSuggestionListItem>{t('Search_for_a_more_general_term')}</StatesSuggestionListItem> + <StatesSuggestionListItem>{t('Search_for_a_more_specific_term')}</StatesSuggestionListItem> + <StatesSuggestionListItem>{t('Check_if_the_spelling_is_correct')}</StatesSuggestionListItem> + </StatesSuggestionList> + </StatesSuggestion> + </States> + </Box> + ); +}; + +export default NoMarketplaceOrInstalledAppMatchesEmptyState; diff --git a/apps/meteor/client/views/admin/apps/components/RadioButtonList.tsx b/apps/meteor/client/views/admin/apps/components/RadioButtonList.tsx index cc16422d279..6d38e81d25e 100644 --- a/apps/meteor/client/views/admin/apps/components/RadioButtonList.tsx +++ b/apps/meteor/client/views/admin/apps/components/RadioButtonList.tsx @@ -1,9 +1,9 @@ import { Box, Option, RadioButton, Tile } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; +import React, { ReactElement } from 'react'; import { RadioDropDownProps } from '../definitions/RadioDropDownDefinitions'; -const RadioButtonList: FC<RadioDropDownProps> = ({ group, onSelected }) => ( +const RadioButtonList = ({ group, onSelected }: RadioDropDownProps): ReactElement => ( <Tile overflow='auto' pb='x12' pi={0} elevation='2' w='full' bg='alternative' borderRadius='x2'> {group.label && ( <Box pi='x16' pbs='x8' pbe='x4' fontScale='micro' textTransform='uppercase' color='default'> diff --git a/apps/meteor/client/views/admin/apps/components/RadioDropDown/RadioDropDown.tsx b/apps/meteor/client/views/admin/apps/components/RadioDropDown/RadioDropDown.tsx index 3536d26e2ee..814154bb94d 100644 --- a/apps/meteor/client/views/admin/apps/components/RadioDropDown/RadioDropDown.tsx +++ b/apps/meteor/client/views/admin/apps/components/RadioDropDown/RadioDropDown.tsx @@ -1,6 +1,6 @@ import { Select } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; -import React, { ComponentProps, FC, useCallback, useRef } from 'react'; +import React, { ComponentProps, ReactElement, useCallback, useRef } from 'react'; import { RadioDropDownProps } from '../../definitions/RadioDropDownDefinitions'; import { isValidReference } from '../../helpers/isValidReference'; @@ -9,7 +9,7 @@ import DropDownListWrapper from '../DropDownListWrapper'; import RadioButtonList from '../RadioButtonList'; import RadioDropDownAnchor from './RadioDownAnchor'; -const RadioDropDown: FC<RadioDropDownProps & Partial<ComponentProps<typeof Select>>> = ({ group, onSelected, ...props }) => { +const RadioDropDown = ({ group, onSelected, ...props }: RadioDropDownProps & Partial<ComponentProps<typeof Select>>): ReactElement => { const reference = useRef<HTMLInputElement>(null); const [collapsed, toggleCollapsed] = useToggle(false); diff --git a/apps/meteor/client/views/admin/apps/helpers/normalizeFeaturedApps.ts b/apps/meteor/client/views/admin/apps/helpers/normalizeFeaturedApps.ts new file mode 100644 index 00000000000..2700f60cbb2 --- /dev/null +++ b/apps/meteor/client/views/admin/apps/helpers/normalizeFeaturedApps.ts @@ -0,0 +1,10 @@ +import { App } from '@rocket.chat/core-typings'; +import type { AppOverview } from '@rocket.chat/core-typings'; + +const normalizeFeaturedApps = (appOverviewList: AppOverview[], appsResultItems: App[]): App[] => { + const featuredAppsIdList = appOverviewList.map<string>((featuredApp) => featuredApp.latest.id); + + return appsResultItems.filter((app) => featuredAppsIdList.includes(app.id)); +}; + +export default normalizeFeaturedApps; diff --git a/apps/meteor/client/views/admin/apps/hooks/useFeaturedApps.ts b/apps/meteor/client/views/admin/apps/hooks/useFeaturedApps.ts new file mode 100644 index 00000000000..fb241535cf5 --- /dev/null +++ b/apps/meteor/client/views/admin/apps/hooks/useFeaturedApps.ts @@ -0,0 +1,9 @@ +import { OperationResult } from '@rocket.chat/rest-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; + +export const useFeaturedApps = (): UseQueryResult<OperationResult<'GET', '/apps/featured-apps'>> => { + const featuredApps = useEndpoint('GET', '/apps/featured-apps'); + + return useQuery(['featured-apps'], () => featuredApps()); +}; diff --git a/apps/meteor/client/views/admin/settings/SettingsPage.tsx b/apps/meteor/client/views/admin/settings/SettingsPage.tsx index 0720043d25a..cd753788f11 100644 --- a/apps/meteor/client/views/admin/settings/SettingsPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsPage.tsx @@ -17,7 +17,7 @@ const SettingsPage = (): ReactElement => { const isLoadingGroups = useIsSettingsContextLoading(); return ( - <Page backgroundColor='neutral-100'> + <Page background='tint'> <Page.Header title={t('Settings')} borderBlockEndColor='' /> <PageBlockWithBorder> diff --git a/apps/meteor/client/views/home/DefaultHomePage.tsx b/apps/meteor/client/views/home/DefaultHomePage.tsx index a3fcd8495d9..8b7a74e56b4 100644 --- a/apps/meteor/client/views/home/DefaultHomePage.tsx +++ b/apps/meteor/client/views/home/DefaultHomePage.tsx @@ -23,7 +23,7 @@ const DefaultHomePage = (): ReactElement => { const workspaceName = useSetting('Site_Name'); return ( - <Page data-qa='page-home' data-qa-type='default' backgroundColor='neutral-100'> + <Page data-qa='page-home' data-qa-type='default' background='tint'> <HomePageHeader /> <PageScrollableContent> <Box is='h1' fontScale='h1' data-qa-id='homepage-welcome-text'> diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index d11fb25daac..790a4d2bf24 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -32,7 +32,7 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb ); return ( - <Page backgroundColor='neutral-100'> + <Page background='tint'> <Page.Header title={t('Engagement_Dashboard')}> <Select options={timezoneOptions} value={timezoneId} onChange={handleTimezoneChange} /> </Page.Header> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 1f8c8aefa11..a2b9c2610c0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3092,6 +3092,18 @@ "Marketplace_app_last_updated": "Last updated __lastUpdated__", "Marketplace_view_marketplace": "View Marketplace", "Marketplace_error": "Cannot connect to internet or your workspace may be an offline install.", + "marketplace_featured_section_community_featured": "Featured Community Apps", + "marketplace_featured_section_community_supported": "Community Supported Apps", + "marketplace_featured_section_enterprise": "Featured Enterprise Apps", + "marketplace_featured_section_featured": "Featured Apps", + "marketplace_featured_section_most_popular": "Most Popular Apps", + "marketplace_featured_section_new_arrivals": "New Arrivals", + "marketplace_featured_section_popular_this_month": "Apps Popular this Month", + "marketplace_featured_section_recommended": "Recommended Apps", + "marketplace_featured_section_social": "Social Apps", + "marketplace_featured_section_trending": "Trending Apps", + "marketplace_featured_section_omnichannel": "Omnichannel Apps", + "marketplace_featured_section_video_conferencing": "Video Conferencing Apps", "MAU_value": "MAU __value__", "Max_length_is": "Max length is %s", "Max_number_incoming_livechats_displayed": "Max number of items displayed in the queue", diff --git a/packages/core-typings/src/AppOverview.ts b/packages/core-typings/src/AppOverview.ts new file mode 100644 index 00000000000..0b0eec45192 --- /dev/null +++ b/packages/core-typings/src/AppOverview.ts @@ -0,0 +1,115 @@ +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import type { App, AppSubscriptionStatus } from './Apps'; + +export type AppOverview = { + appId: string; + latest: Latest; + isAddon: boolean; + addonId: string; + isEnterpriseOnly: boolean; + isBundle: boolean; + bundledAppIds: any[]; + bundledIn: bundledIn[]; + isPurchased: boolean; + isSubscribed: boolean; + subscriptionInfo: SubscriptionInfo; + price: number; + purchaseType: string; + pricingPlans: PricingPlan[]; + isUsageBased: boolean; + createdAt: string; + modifiedAt: string; + iconFileContent?: string; + marketplaceVersion?: string; + marketplace?: unknown; +}; + +export type bundledIn = { + bundleId: string; + bundleName: string; + apps: App[]; + addonTierId: string; +}; + +export type Latest = { + internalId: string; + id: string; + name: string; + nameSlug: string; + version: string; + categories: string[]; + languages: string[]; + description: string; + privacyPolicySummary: string; + detailedDescription: DetailedDescription; + detailedChangelog: DetailedChangelog; + requiredApiVersion: string; + permissions: Permission[]; + author: Author; + classFile: string; + iconFile: string; + iconFileData: string; + status: AppStatus; + isVisible: boolean; + createdDate: string; + modifiedDate: string; + isPurchased: boolean; + isSubscribed: boolean; + subscriptionInfo: SubscriptionInfo; + compileJobId: string; + compiled: boolean; + tosLink: string; + privacyLink: string; +}; + +export type DetailedDescription = { + raw: string; + rendered: string; +}; + +export type DetailedChangelog = { + raw: string; + rendered: string; +}; + +export type Permission = { + name: string; + scopes?: string[]; +}; + +export type Author = { + name: string; + support: string; + homepage: string; +}; + +export type SubscriptionInfo = { + typeOf: string; + status: AppSubscriptionStatus; + statusFromBilling: boolean; + isSeatBased: boolean; + seats: number; + maxSeats: number; + license: License; + startDate: string; + periodEnd: string; + endDate: string; + externallyManaged: boolean; + isSubscribedViaBundle: boolean; +}; + +export type License = { + license: string; + version: number; + expireDate: string; +}; + +export type PricingPlan = { + id: string; + enabled: boolean; + price: number; + trialDays: number; + strategy: string; + isPerSeat: boolean; +}; diff --git a/packages/core-typings/src/Apps.ts b/packages/core-typings/src/Apps.ts index 9d91f9baeb9..1b181504cc8 100644 --- a/packages/core-typings/src/Apps.ts +++ b/packages/core-typings/src/Apps.ts @@ -104,7 +104,7 @@ export type App = { }; tosLink: string; privacyLink: string; - marketplace: unknown; + marketplace?: unknown; modifiedAt: string; permissions: AppPermission[]; languages: string[]; diff --git a/packages/core-typings/src/FeaturedApps.ts b/packages/core-typings/src/FeaturedApps.ts index efe32300abe..462c2af93c7 100644 --- a/packages/core-typings/src/FeaturedApps.ts +++ b/packages/core-typings/src/FeaturedApps.ts @@ -1,11 +1,7 @@ -import type { App } from './Apps'; - -export type FeaturedAppsSections = { - sections: FeaturedAppsSection[]; -}; +import type { AppOverview } from './AppOverview'; export type FeaturedAppsSection = { i18nLabel: string; slug: string; - apps: App[]; + apps: AppOverview[]; }; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 17cd6b5bab9..cf1d2babedd 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -1,4 +1,5 @@ export * from './Apps'; +export * from './AppOverview'; export * from './FeaturedApps'; export * from './IRoom'; export * from './UIKit'; diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 6b7e5b8e043..daafb732c73 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -4,7 +4,7 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/ext import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import type { AppScreenshot, App, FeaturedAppsSections } from '@rocket.chat/core-typings'; +import type { AppScreenshot, App, FeaturedAppsSection } from '@rocket.chat/core-typings'; export type AppsEndpoints = { '/apps/externalComponents': { @@ -96,9 +96,9 @@ export type AppsEndpoints = { }; }; - '/apps/featured': { + '/apps/featured-apps': { GET: () => { - sections: FeaturedAppsSections; + sections: FeaturedAppsSection[]; }; }; -- GitLab From 3b35f8bee550d6936a73e601146af7fb0c5739b8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Fri, 9 Sep 2022 18:39:58 -0300 Subject: [PATCH 021/107] Chore: `refactor/room` (#26675) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> Co-authored-by: Diego Sampaio <chinello@gmail.com> --- .../autotranslate/client/lib/actionButton.ts | 8 + .../client/createDiscussionMessageAction.ts | 4 + .../server/methods/sendFileMessage.ts | 4 +- .../integrations/server/lib/triggerHandler.js | 3 +- .../mentions-flextab/client/actionButton.ts | 2 +- .../client/actionButton.ts | 5 + .../app/message-pin/client/actionButton.ts | 2 +- .../app/message-star/client/actionButton.ts | 10 +- .../meteor-autocomplete/client/templates.js | 4 +- .../app/models/server/models/_BaseDb.js | 4 +- .../app/search/client/provider/result.js | 4 +- .../theme/client/imports/general/base_old.css | 29 - .../app/theme/client/imports/general/rtl.css | 5 - .../threads/client/messageAction/follow.ts | 2 +- .../threads/client/messageAction/unfollow.ts | 2 +- .../client/models/CachedCollection.js | 3 +- .../app/ui-utils/client/lib/MessageAction.ts | 2 +- .../ui-utils/client/lib/RoomHistoryManager.ts | 22 +- .../client/lib/messageActionDefault.ts | 6 +- .../app/ui-utils/client/lib/messageContext.ts | 192 ++++-- .../app/ui-utils/client/lib/openRoom.tsx | 18 +- apps/meteor/app/ui/client/index.ts | 1 - .../app/lib/CommonRoomTemplateInstance.ts | 10 +- .../views/app/lib/RoomTemplateInstance.ts | 44 -- .../views/app/lib/getCommonRoomEvents.ts | 122 ++-- .../ui/client/views/app/lib/mountPopover.ts | 12 +- .../client/views/app/lib/retentionPolicy.ts | 115 ---- .../app/ui/client/views/app/lib/roomEvents.ts | 138 ---- .../ui/client/views/app/lib/roomHelpers.ts | 312 --------- .../views/app/lib/roomLifeCycleMethods.ts | 428 ------------ apps/meteor/app/ui/client/views/app/room.html | 144 ---- apps/meteor/app/ui/client/views/app/room.ts | 27 - .../client/hooks/useFormatRelativeTime.ts | 14 + apps/meteor/client/startup/routes.tsx | 21 +- apps/meteor/client/templates.ts | 84 +-- .../omnichannel/directory/calls/Call.tsx | 16 +- .../omnichannel/directory/chats/Chat.tsx | 16 +- .../views/room/MessageList/MessageList.tsx | 8 +- .../meteor/client/views/room/Room.stories.tsx | 19 - .../views/room/Room/ComposerSkeleton.tsx | 4 +- .../client/views/room/Room/LazyComponent.js | 11 - apps/meteor/client/views/room/Room/Room.tsx | 91 ++- .../views/room/Room/RoomNotFound/index.ts | 1 - .../client/views/room/Room/RoomSkeleton.tsx | 54 -- .../client/views/room/Room/RoomWithData.tsx | 16 - apps/meteor/client/views/room/Room/index.ts | 1 - .../{Room/RoomNotFound => }/RoomNotFound.tsx | 22 +- .../meteor/client/views/room/RoomSkeleton.tsx | 54 ++ .../components/RoomTemplate/RoomTemplate.tsx | 44 -- .../components/RoomTemplate/slots/Aside.tsx | 5 - .../components/RoomTemplate/slots/Body.tsx | 5 - .../components/RoomTemplate/slots/Footer.tsx | 5 - .../components/RoomTemplate/slots/Header.tsx | 5 - .../components/body/ComposerContainer.tsx | 104 +++ .../components/body/DropTargetOverlay.tsx | 95 +++ .../body/ErroredUploadProgressIndicator.tsx | 29 + .../body/JumpToRecentMessagesBar.tsx | 23 + .../views/room/components/body/LeaderBar.tsx | 62 ++ .../body/LegacyMessageTemplateList.tsx | 90 +++ .../body/LoadingMessagesIndicator.tsx | 11 + .../components/body/NewMessagesButton.tsx | 28 + .../body/RetentionPolicyWarning.tsx | 38 ++ .../views/room/components/body/RoomBody.tsx | 631 ++++++++++++++++++ .../room/components/body}/RoomForeword.tsx | 38 +- .../body/UnreadMessagesIndicator.tsx | 37 + .../body/UploadProgressIndicator.tsx | 39 ++ .../room/components/body/useChatMessages.ts | 27 + .../room/components/body/useDropTarget.ts | 40 ++ .../body/useFileUploadDropTarget.ts | 88 +++ .../room/components/body/useMessageContext.ts | 94 +++ .../components/body/useRetentionPolicy.ts | 104 +++ .../room/components/body/useRoomRoles.ts | 58 ++ .../room/components/body/useUnreadMessages.ts | 32 + apps/meteor/client/views/room/index.ts | 6 + .../client/views/room/layout/RoomLayout.tsx | 28 + .../client/views/room/lib/Toolbox/index.tsx | 12 +- .../views/room/providers/RoomProvider.tsx | 6 +- .../views/room/providers/ToolboxProvider.tsx | 9 +- apps/meteor/lib/callbacks.ts | 4 +- apps/meteor/lib/utils/comparisons.ts | 30 + apps/meteor/lib/utils/highOrderFunctions.ts | 69 ++ apps/meteor/lib/utils/omit.ts | 7 + apps/meteor/server/lib/ldap/Manager.ts | 3 +- apps/meteor/server/lib/logger/logPayloads.ts | 6 +- .../services/omnichannel-voip/service.ts | 1 + .../page-objects/fragments/home-content.ts | 8 +- packages/core-typings/src/IInquiry.ts | 3 +- packages/core-typings/src/IRoom.ts | 14 +- 88 files changed, 2249 insertions(+), 1805 deletions(-) delete mode 100644 apps/meteor/app/ui/client/views/app/lib/RoomTemplateInstance.ts delete mode 100644 apps/meteor/app/ui/client/views/app/lib/retentionPolicy.ts delete mode 100644 apps/meteor/app/ui/client/views/app/lib/roomEvents.ts delete mode 100644 apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts delete mode 100644 apps/meteor/app/ui/client/views/app/lib/roomLifeCycleMethods.ts delete mode 100644 apps/meteor/app/ui/client/views/app/room.html delete mode 100644 apps/meteor/app/ui/client/views/app/room.ts create mode 100644 apps/meteor/client/hooks/useFormatRelativeTime.ts delete mode 100644 apps/meteor/client/views/room/Room.stories.tsx delete mode 100644 apps/meteor/client/views/room/Room/LazyComponent.js delete mode 100644 apps/meteor/client/views/room/Room/RoomNotFound/index.ts delete mode 100644 apps/meteor/client/views/room/Room/RoomSkeleton.tsx delete mode 100644 apps/meteor/client/views/room/Room/RoomWithData.tsx delete mode 100644 apps/meteor/client/views/room/Room/index.ts rename apps/meteor/client/views/room/{Room/RoomNotFound => }/RoomNotFound.tsx (75%) create mode 100644 apps/meteor/client/views/room/RoomSkeleton.tsx delete mode 100644 apps/meteor/client/views/room/components/RoomTemplate/RoomTemplate.tsx delete mode 100644 apps/meteor/client/views/room/components/RoomTemplate/slots/Aside.tsx delete mode 100644 apps/meteor/client/views/room/components/RoomTemplate/slots/Body.tsx delete mode 100644 apps/meteor/client/views/room/components/RoomTemplate/slots/Footer.tsx delete mode 100644 apps/meteor/client/views/room/components/RoomTemplate/slots/Header.tsx create mode 100644 apps/meteor/client/views/room/components/body/ComposerContainer.tsx create mode 100644 apps/meteor/client/views/room/components/body/DropTargetOverlay.tsx create mode 100644 apps/meteor/client/views/room/components/body/ErroredUploadProgressIndicator.tsx create mode 100644 apps/meteor/client/views/room/components/body/JumpToRecentMessagesBar.tsx create mode 100644 apps/meteor/client/views/room/components/body/LeaderBar.tsx create mode 100644 apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx create mode 100644 apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx create mode 100644 apps/meteor/client/views/room/components/body/NewMessagesButton.tsx create mode 100644 apps/meteor/client/views/room/components/body/RetentionPolicyWarning.tsx create mode 100644 apps/meteor/client/views/room/components/body/RoomBody.tsx rename apps/meteor/client/{components => views/room/components/body}/RoomForeword.tsx (54%) create mode 100644 apps/meteor/client/views/room/components/body/UnreadMessagesIndicator.tsx create mode 100644 apps/meteor/client/views/room/components/body/UploadProgressIndicator.tsx create mode 100644 apps/meteor/client/views/room/components/body/useChatMessages.ts create mode 100644 apps/meteor/client/views/room/components/body/useDropTarget.ts create mode 100644 apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts create mode 100644 apps/meteor/client/views/room/components/body/useMessageContext.ts create mode 100644 apps/meteor/client/views/room/components/body/useRetentionPolicy.ts create mode 100644 apps/meteor/client/views/room/components/body/useRoomRoles.ts create mode 100644 apps/meteor/client/views/room/components/body/useUnreadMessages.ts create mode 100644 apps/meteor/client/views/room/index.ts create mode 100644 apps/meteor/client/views/room/layout/RoomLayout.tsx create mode 100644 apps/meteor/lib/utils/comparisons.ts create mode 100644 apps/meteor/lib/utils/highOrderFunctions.ts create mode 100644 apps/meteor/lib/utils/omit.ts diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 0d742d68f9a..eb8cba74c34 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -32,6 +32,10 @@ Meteor.startup(() => { Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, condition({ message, user }) { + if (!user) { + return false; + } + return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && message.autoTranslateShowInverse); }, order: 90, @@ -54,6 +58,10 @@ Meteor.startup(() => { Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, condition({ message, user }) { + if (!user) { + return false; + } + return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && !message.autoTranslateShowInverse); }, order: 90, diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts index 2b99ff50a02..785a2114b04 100644 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts @@ -54,6 +54,10 @@ Meteor.startup(function () { return false; } + if (!user) { + return false; + } + return uid !== user._id ? hasPermission('start-discussion-other-user') : hasPermission('start-discussion'); }, order: 1, diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index a3f6332f1cf..5f605323d04 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import _ from 'underscore'; import type { MessageAttachment, FileAttachmentProps, IUser } from '@rocket.chat/core-typings'; import { Rooms, Uploads } from '@rocket.chat/models'; @@ -8,6 +7,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { FileUpload } from '../lib/FileUpload'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { omit } from '../../../../lib/utils/omit'; Meteor.methods({ async sendFileMessage(roomId, _store, file, msgData = {}) { @@ -36,7 +36,7 @@ Meteor.methods({ tmid: Match.Optional(String), }); - await Uploads.updateFileComplete(file._id, user._id, _.omit(file, '_id')); + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`); diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index a44deaa537d..bfa3c662b99 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -15,6 +15,7 @@ import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../ import { outgoingLogger } from '../logger'; import { outgoingEvents } from '../../lib/outgoingEvents'; import { fetch } from '../../../../server/lib/http/fetch'; +import { omit } from '../../../../lib/utils/omit'; export class RocketChatIntegrationHandler { constructor() { @@ -103,7 +104,7 @@ export class RocketChatIntegrationHandler { history.data = { ...data }; if (data.user) { - history.data.user = _.omit(data.user, ['services']); + history.data.user = omit(data.user, 'services'); } if (data.room) { diff --git a/apps/meteor/app/mentions-flextab/client/actionButton.ts b/apps/meteor/app/mentions-flextab/client/actionButton.ts index 11168a8cae7..33bfff0ac86 100644 --- a/apps/meteor/app/mentions-flextab/client/actionButton.ts +++ b/apps/meteor/app/mentions-flextab/client/actionButton.ts @@ -33,7 +33,7 @@ Meteor.startup(function () { }, ); } - RoomHistoryManager.getSurroundingMessages(message, 50); + RoomHistoryManager.getSurroundingMessages(message); }, order: 100, group: ['message', 'menu'], diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index bbf558e65e4..93e56085d70 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -35,6 +35,11 @@ Meteor.startup(() => { if (isLivechatRoom) { return false; } + + if (!user) { + return false; + } + return message.u._id !== user._id; }, order: 10, diff --git a/apps/meteor/app/message-pin/client/actionButton.ts b/apps/meteor/app/message-pin/client/actionButton.ts index e316304dca6..4fd986f50d2 100644 --- a/apps/meteor/app/message-pin/client/actionButton.ts +++ b/apps/meteor/app/message-pin/client/actionButton.ts @@ -90,7 +90,7 @@ Meteor.startup(function () { }, ); } - return RoomHistoryManager.getSurroundingMessages(message, 50); + return RoomHistoryManager.getSurroundingMessages(message); }, condition({ subscription }) { return !!subscription; diff --git a/apps/meteor/app/message-star/client/actionButton.ts b/apps/meteor/app/message-star/client/actionButton.ts index eaf9793caff..f8776805a12 100644 --- a/apps/meteor/app/message-star/client/actionButton.ts +++ b/apps/meteor/app/message-star/client/actionButton.ts @@ -33,7 +33,7 @@ Meteor.startup(function () { return false; } - return !Array.isArray(message.starred) || !message.starred.find((star: any) => star._id === user._id); + return !Array.isArray(message.starred) || !message.starred.find((star: any) => star._id === user?._id); }, order: 9, group: 'menu', @@ -58,7 +58,7 @@ Meteor.startup(function () { return false; } - return Boolean(message.starred?.find((star: any) => star._id === user._id)); + return Boolean(message.starred?.find((star: any) => star._id === user?._id)); }, order: 9, group: 'menu', @@ -90,14 +90,14 @@ Meteor.startup(function () { }, ); } - RoomHistoryManager.getSurroundingMessages(message, 50); + RoomHistoryManager.getSurroundingMessages(message); }, condition({ message, subscription, user }) { if (subscription == null || !settings.get('Message_AllowStarring')) { return false; } - return Boolean(message.starred?.find((star) => star._id === user._id)); + return Boolean(message.starred?.find((star) => star._id === user?._id)); }, order: 100, group: ['message', 'menu'], @@ -120,7 +120,7 @@ Meteor.startup(function () { return false; } - return Boolean(message.starred?.find((star) => star._id === user._id)); + return Boolean(message.starred?.find((star) => star._id === user?._id)); }, order: 101, group: 'menu', diff --git a/apps/meteor/app/meteor-autocomplete/client/templates.js b/apps/meteor/app/meteor-autocomplete/client/templates.js index 1afb054c9eb..c2ef9cd62ed 100755 --- a/apps/meteor/app/meteor-autocomplete/client/templates.js +++ b/apps/meteor/app/meteor-autocomplete/client/templates.js @@ -1,7 +1,7 @@ import { Blaze } from 'meteor/blaze'; import { Template } from 'meteor/templating'; -import _ from 'underscore'; +import { omit } from '../../../lib/utils/omit'; import AutoComplete from './autocomplete-client'; // Events on template instances, sent to the autocomplete class @@ -25,7 +25,7 @@ Template.inputAutocomplete.events(acEvents); Template.textareaAutocomplete.events(acEvents); const attributes = function () { - return _.omit(this, 'settings'); // Render all but the settings parameter + return omit(this, 'settings'); // Render all but the settings parameter }; const autocompleteHelpers = { diff --git a/apps/meteor/app/models/server/models/_BaseDb.js b/apps/meteor/app/models/server/models/_BaseDb.js index b7d1b12d473..285c2a78ded 100644 --- a/apps/meteor/app/models/server/models/_BaseDb.js +++ b/apps/meteor/app/models/server/models/_BaseDb.js @@ -1,9 +1,9 @@ import { Match } from 'meteor/check'; import { Mongo } from 'meteor/mongo'; -import _ from 'underscore'; import { setUpdatedAt } from '../lib/setUpdatedAt'; import { trash } from '../../../../server/database/trash'; +import { omit } from '../../../../lib/utils/omit'; const baseName = 'rocketchat_'; @@ -168,7 +168,7 @@ export class BaseDb { record._deletedAt = new Date(); record.__collection__ = this.name; - trash.upsert({ _id: record._id }, _.omit(record, '_id')); + trash.upsert({ _id: record._id }, omit(record, '_id')); } query = { _id: { $in: ids } }; diff --git a/apps/meteor/app/search/client/provider/result.js b/apps/meteor/app/search/client/provider/result.js index ec0ca30f978..03c00c69fda 100644 --- a/apps/meteor/app/search/client/provider/result.js +++ b/apps/meteor/app/search/client/provider/result.js @@ -37,7 +37,7 @@ Meteor.startup(function () { } if (Session.get('openedRoom') === message.rid) { - return RoomHistoryManager.getSurroundingMessages(message, 50); + return RoomHistoryManager.getSurroundingMessages(message); } goToRoomById(message.rid); @@ -48,7 +48,7 @@ Meteor.startup(function () { } window.setTimeout(() => { - RoomHistoryManager.getSurroundingMessages(message, 50); + RoomHistoryManager.getSurroundingMessages(message); }, 400); // 400ms is popular among game devs as a good delay before transition starts // ie. 50, 100, 200, 400, 800 are the favored timings diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 23450521d1c..ee013f04e46 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -1394,35 +1394,6 @@ } } -.rc-old .ticks-bar { - position: absolute; - z-index: 1; - right: 2px; - - width: 10px; - height: 100%; - - pointer-events: none; - - & .tick { - position: absolute; - top: 50%; - - width: 100%; - height: 2px; - - border-radius: 2px; - - &--me { - background-color: var(--mention-link-me-text-color); - } - - &--group { - background-color: var(--mention-link-group-text-color); - } - } -} - .message { position: relative; z-index: 1; diff --git a/apps/meteor/app/theme/client/imports/general/rtl.css b/apps/meteor/app/theme/client/imports/general/rtl.css index e726921f068..6ac1a37e26a 100644 --- a/apps/meteor/app/theme/client/imports/general/rtl.css +++ b/apps/meteor/app/theme/client/imports/general/rtl.css @@ -299,11 +299,6 @@ } } - & .ticks-bar { - right: auto; - left: 2px; - } - & .fixed-title { right: 0; left: auto; diff --git a/apps/meteor/app/threads/client/messageAction/follow.ts b/apps/meteor/app/threads/client/messageAction/follow.ts index 4b8827fd5f1..88494977058 100644 --- a/apps/meteor/app/threads/client/messageAction/follow.ts +++ b/apps/meteor/app/threads/client/messageAction/follow.ts @@ -42,7 +42,7 @@ Meteor.startup(function () { if (isLivechatRoom) { return false; } - return !replies.includes(user._id); + return user?._id ? !replies.includes(user._id) : false; }, order: 2, group: 'menu', diff --git a/apps/meteor/app/threads/client/messageAction/unfollow.ts b/apps/meteor/app/threads/client/messageAction/unfollow.ts index fb59a8e74b6..3d3ce26d647 100644 --- a/apps/meteor/app/threads/client/messageAction/unfollow.ts +++ b/apps/meteor/app/threads/client/messageAction/unfollow.ts @@ -36,7 +36,7 @@ Meteor.startup(function () { replies = parentMessage.replies || []; } } - return replies.includes(user._id); + return user?._id ? replies.includes(user._id) : false; }, order: 2, group: 'menu', diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js index 3da924f7167..0d047fe2dd2 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js @@ -12,6 +12,7 @@ import { callbacks } from '../../../../lib/callbacks'; import Notifications from '../../../notifications/client/lib/Notifications'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { call } from '../../../../client/lib/utils/call'; +import { omit } from '../../../../lib/utils/omit'; const wrap = (fn) => @@ -228,7 +229,7 @@ export class CachedCollection extends Emitter { this.log(`${data.length} records loaded from server`); data.forEach((record) => { callbacks.run(`cachedCollection-loadFromServer-${this.name}`, record, 'changed'); - this.collection.direct.upsert({ _id: record._id }, _.omit(record, '_id')); + this.collection.direct.upsert({ _id: record._id }, omit(record, '_id')); this.onSyncData('changed', record); diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 394431efabd..8fa25ecc88b 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -36,7 +36,7 @@ type MessageActionContext = 'message' | 'threads' | 'message-mobile' | 'pinned' type MessageActionConditionProps = { message: IMessage; - user: IUser; + user: IUser | undefined; room: IRoom; subscription?: ISubscription; context?: MessageActionContext; diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 8e477ca074c..627ef606369 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { ReactiveVar } from 'meteor/reactive-var'; -import { Blaze } from 'meteor/blaze'; import { v4 as uuidv4 } from 'uuid'; import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'; import { Emitter } from '@rocket.chat/emitter'; import type { IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { MutableRefObject } from 'react'; import { waitUntilWrapperExists } from './waitUntilWrapperExists'; import { readMessage } from './readMessages'; @@ -17,7 +17,6 @@ import { setHighlightMessage, clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; -import type { RoomTemplateInstance } from '../../../ui/client/views/app/lib/RoomTemplateInstance'; import { normalizeThreadMessage } from '../../../../client/lib/normalizeThreadMessage'; export async function upsertMessage( @@ -219,15 +218,14 @@ class RoomHistoryManagerClass extends Emitter { }); } - public async getMoreNext(rid: IRoom['_id'], limit = defaultLimit) { + public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject<boolean>) { const room = this.getRoom(rid); if (Tracker.nonreactive(() => room.hasMoreNext.get()) !== true) { return; } await this.queue(); - const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance() as RoomTemplateInstance; - instance.atBottom = false; + atBottomRef.current = false; room.isLoading.set(true); @@ -237,7 +235,7 @@ class RoomHistoryManagerClass extends Emitter { if (lastMessage?.ts) { const { ts } = lastMessage; - const result = await callWithErrorHandling('loadNextMessages', rid, ts, limit); + const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit); upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription, @@ -249,7 +247,7 @@ class RoomHistoryManagerClass extends Emitter { } room.loaded += result.messages.length; - if (result.messages.length < limit) { + if (result.messages.length < defaultLimit) { room.hasMoreNext.set(false); } } @@ -288,15 +286,11 @@ class RoomHistoryManagerClass extends Emitter { room.loaded = undefined; } - public async getSurroundingMessages(message?: Pick<IMessage, '_id' | 'rid'> & { ts?: Date }, limit = defaultLimit) { + public async getSurroundingMessages(message?: Pick<IMessage, '_id' | 'rid'> & { ts?: Date }, atBottomRef?: MutableRefObject<boolean>) { if (!message || !message.rid) { return; } - const w = (await waitUntilWrapperExists()) as HTMLElement; - - const instance = Blaze.getView(w).templateInstance() as RoomTemplateInstance; - const surroundingMessage = ChatMessage.findOne({ _id: message._id, _hidden: { $ne: true } }); if (surroundingMessage) { @@ -336,7 +330,7 @@ class RoomHistoryManagerClass extends Emitter { const subscription = ChatSubscription.findOne({ rid: message.rid }); - const result = await callWithErrorHandling('loadSurroundingMessages', message, limit); + const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit); if (!result || !result.messages) { return; @@ -368,7 +362,7 @@ class RoomHistoryManagerClass extends Emitter { room.isLoading.set(false); const messages = wrapper[0]; - instance.atBottom = !result.moreAfter && messages.scrollTop >= messages.scrollHeight - messages.clientHeight; + if (atBottomRef) atBottomRef.current = !result.moreAfter && messages.scrollTop >= messages.scrollHeight - messages.clientHeight; setTimeout(() => { msgElement.removeClass('highlight'); diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index f2c78875dd6..e2800d80bd5 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -61,7 +61,7 @@ Meteor.startup(async function () { } // Check if we already have a DM started with the message user (not ourselves) or we can start one - if (user._id !== message.u._id && !hasPermission('create-d')) { + if (!!user && user._id !== message.u._id && !hasPermission('create-d')) { const dmRoom = Rooms.findOne({ _id: [user._id, message.u._id].sort().join('') }); if (!dmRoom || !Subscriptions.findOne({ 'rid': dmRoom._id, 'u._id': user._id })) { return false; @@ -303,6 +303,10 @@ Meteor.startup(async function () { return false; } + if (!user) { + return false; + } + return uid !== user._id ? hasPermission('start-discussion-other-user') : hasPermission('start-discussion'); }, order: 1, diff --git a/apps/meteor/app/ui-utils/client/lib/messageContext.ts b/apps/meteor/app/ui-utils/client/lib/messageContext.ts index 2ed4f282d4b..9fa97230e5c 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageContext.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageContext.ts @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Tracker } from 'meteor/tracker'; -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { Blaze } from 'meteor/blaze'; import { Subscriptions, Rooms, Users } from '../../../models/client'; import { hasPermission } from '../../../authorization/client'; @@ -14,6 +15,7 @@ import { actionLinks } from '../../../action-links/client'; import { goToRoomById } from '../../../../client/lib/utils/goToRoomById'; import { isLayoutEmbedded } from '../../../../client/lib/utils/isLayoutEmbedded'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +import type { CommonRoomTemplateInstance } from '../../../ui/client/views/app/lib/CommonRoomTemplateInstance'; const fields = { 'name': 1, @@ -25,14 +27,94 @@ const fields = { 'settings.preferences.hideRoles': 1, }; -export function messageContext( - { rid }: { rid: IRoom['_id'] } = Template.instance() as Blaze.TemplateInstance<Record<string, unknown>> & { rid: IRoom['_id'] }, -) { - const uid = Meteor.userId(); - const user = Users.findOne({ _id: uid }, { fields }) || {}; - const instance = Template.instance(); - const openThread = (e: Event) => { - const { rid, mid, tmid } = (e.currentTarget as HTMLElement | null)?.dataset ?? {}; +export const createMessageContext = ({ + uid = Meteor.userId(), + user = Users.findOne({ _id: uid }, { fields }) || {}, + rid = (Template.instance() as CommonRoomTemplateInstance).data.rid, + room = Tracker.nonreactive(() => + Rooms.findOne( + { _id: rid }, + { + fields: { + _updatedAt: 0, + lastMessage: 0, + }, + }, + ), + ), + subscription = Subscriptions.findOne( + { rid }, + { + fields: { + name: 1, + autoTranslate: 1, + rid: 1, + tunread: 1, + tunreadUser: 1, + tunreadGroup: 1, + }, + }, + ), + instance = Template.instance(), + embeddedLayout = isLayoutEmbedded(), + translateLanguage = AutoTranslate.getLanguage(rid), + autoImageLoad = getUserPreference(user, 'autoImageLoad'), + useLegacyMessageTemplate = getUserPreference(user, 'useLegacyMessageTemplate'), + saveMobileBandwidth = Meteor.Device.isPhone() && getUserPreference(user, 'saveMobileBandwidth'), + collapseMediaByDefault = getUserPreference(user, 'collapseMediaByDefault'), + showreply = true, + showReplyButton = true, + hasPermissionDeleteMessage = hasPermission('delete-message', rid), + hasPermissionDeleteOwnMessage = hasPermission('delete-own-message'), + hideRoles = !settings.get('UI_DisplayRoles') || getUserPreference(user, 'hideRoles'), + // eslint-disable-next-line @typescript-eslint/naming-convention + UI_Use_Real_Name = settings.get('UI_Use_Real_Name'), + // eslint-disable-next-line @typescript-eslint/naming-convention + Chatops_Username = settings.get('Chatops_Username'), + // eslint-disable-next-line @typescript-eslint/naming-convention + AutoTranslate_Enabled = settings.get('AutoTranslate_Enabled'), + // eslint-disable-next-line @typescript-eslint/naming-convention + Message_AllowEditing = settings.get('Message_AllowEditing'), + // eslint-disable-next-line @typescript-eslint/naming-convention + Message_AllowEditing_BlockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'), + // eslint-disable-next-line @typescript-eslint/naming-convention + Message_ShowEditedStatus = settings.get('Message_ShowEditedStatus'), + // eslint-disable-next-line @typescript-eslint/naming-convention + API_Embed = settings.get('API_Embed'), + // eslint-disable-next-line @typescript-eslint/naming-convention + API_EmbedDisabledFor = settings.get('API_EmbedDisabledFor'), + // eslint-disable-next-line @typescript-eslint/naming-convention + Message_GroupingPeriod = settings.get('Message_GroupingPeriod') * 1000, +}: { + uid?: IUser['_id'] | null; + user?: Partial<IUser>; + rid?: IRoom['_id']; + room?: Omit<IRoom, '_updatedAt' | 'lastMessage'>; + subscription?: Pick<ISubscription, 'name' | 'autoTranslate' | 'rid' | 'tunread' | 'tunreadUser' | 'tunreadGroup'>; + instance?: Blaze.TemplateInstance | ((actionId: string, context: string) => void); + embeddedLayout?: boolean; + translateLanguage?: unknown; + autoImageLoad?: unknown; + useLegacyMessageTemplate?: unknown; + saveMobileBandwidth?: unknown; + collapseMediaByDefault?: unknown; + showreply?: unknown; + showReplyButton?: unknown; + hasPermissionDeleteMessage?: unknown; + hasPermissionDeleteOwnMessage?: unknown; + hideRoles?: unknown; + UI_Use_Real_Name?: unknown; + Chatops_Username?: unknown; + AutoTranslate_Enabled?: unknown; + Message_AllowEditing?: unknown; + Message_AllowEditing_BlockEditInMinutes?: unknown; + Message_ShowEditedStatus?: unknown; + API_Embed?: unknown; + API_EmbedDisabledFor?: unknown; + Message_GroupingPeriod?: unknown; +}) => { + const openThread = (event: Event) => { + const { rid, mid, tmid } = (event.currentTarget as HTMLElement | null)?.dataset ?? {}; if (!rid) { throw new Error('Missing rid'); } @@ -58,11 +140,11 @@ export function messageContext( } : {}, ); - e.preventDefault(); - e.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); }; - const runAction = isLayoutEmbedded() + const runAction = embeddedLayout ? (msg: IMessage, actionlink: string) => fireGlobalEvent('click-action-link', { actionlink, @@ -71,18 +153,18 @@ export function messageContext( }) : (msg: IMessage, actionlink: string) => actionLinks.run(actionlink, msg, instance); - const openDiscussion = (e: Event) => { - e.preventDefault(); - e.stopPropagation(); - const { drid } = (e.currentTarget as HTMLElement | null)?.dataset ?? {}; + const openDiscussion = (event: Event) => { + event.preventDefault(); + event.stopPropagation(); + const { drid } = (event.currentTarget as HTMLElement | null)?.dataset ?? {}; if (!drid) { throw new Error('Missing drid'); } goToRoomById(drid); }; - const replyBroadcast = (e: Event) => { - const { username, mid } = (e.currentTarget as HTMLElement | null)?.dataset ?? {}; + const replyBroadcast = (event: Event) => { + const { username, mid } = (event.currentTarget as HTMLElement | null)?.dataset ?? {}; if (!mid) { throw new Error('Missing mid'); } @@ -91,38 +173,16 @@ export function messageContext( return { u: user, - room: Tracker.nonreactive(() => - Rooms.findOne( - { _id: rid }, - { - fields: { - _updatedAt: 0, - lastMessage: 0, - }, - }, - ), - ), - subscription: Subscriptions.findOne( - { rid }, - { - fields: { - name: 1, - autoTranslate: 1, - rid: 1, - tunread: 1, - tunreadUser: 1, - tunreadGroup: 1, - }, - }, - ), + room, + subscription, actions: { openThread() { return openThread; }, runAction(msg: IMessage) { - return () => (actionlink: string) => (e: Event) => { - e.preventDefault(); - e.stopPropagation(); + return () => (actionlink: string) => (event: Event) => { + event.preventDefault(); + event.stopPropagation(); runAction(msg, actionlink); }; }, @@ -134,25 +194,29 @@ export function messageContext( }, }, settings: { - translateLanguage: AutoTranslate.getLanguage(rid), - autoImageLoad: getUserPreference(user, 'autoImageLoad'), - useLegacyMessageTemplate: getUserPreference(user, 'useLegacyMessageTemplate'), - saveMobileBandwidth: Meteor.Device.isPhone() && getUserPreference(user, 'saveMobileBandwidth'), - collapseMediaByDefault: getUserPreference(user, 'collapseMediaByDefault'), - showreply: true, - showReplyButton: true, - hasPermissionDeleteMessage: hasPermission('delete-message', rid), - hasPermissionDeleteOwnMessage: hasPermission('delete-own-message'), - hideRoles: !settings.get('UI_DisplayRoles') || getUserPreference(user, 'hideRoles'), - UI_Use_Real_Name: settings.get('UI_Use_Real_Name'), - Chatops_Username: settings.get('Chatops_Username'), - AutoTranslate_Enabled: settings.get('AutoTranslate_Enabled'), - Message_AllowEditing: settings.get('Message_AllowEditing'), - Message_AllowEditing_BlockEditInMinutes: settings.get('Message_AllowEditing_BlockEditInMinutes'), - Message_ShowEditedStatus: settings.get('Message_ShowEditedStatus'), - API_Embed: settings.get('API_Embed'), - API_EmbedDisabledFor: settings.get('API_EmbedDisabledFor'), - Message_GroupingPeriod: settings.get('Message_GroupingPeriod') * 1000, + translateLanguage, + autoImageLoad, + useLegacyMessageTemplate, + saveMobileBandwidth, + collapseMediaByDefault, + showreply, + showReplyButton, + hasPermissionDeleteMessage, + hasPermissionDeleteOwnMessage, + hideRoles, + UI_Use_Real_Name, + Chatops_Username, + AutoTranslate_Enabled, + Message_AllowEditing, + Message_AllowEditing_BlockEditInMinutes, + Message_ShowEditedStatus, + API_Embed, + API_EmbedDisabledFor, + Message_GroupingPeriod, }, } as const; +}; + +export function messageContext({ rid }: { rid?: IRoom['_id'] } = {}) { + return createMessageContext({ rid }); } diff --git a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx index f46fd83b36b..62d52b2f379 100644 --- a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx +++ b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Session } from 'meteor/session'; -import _ from 'underscore'; import type { RoomType } from '@rocket.chat/core-typings'; import { appLayout } from '../../../../client/lib/appLayout'; @@ -18,7 +17,8 @@ import { RoomManager as NewRoomManager } from '../../../../client/lib/RoomManage import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import MainLayout from '../../../../client/views/root/MainLayout'; -import BlazeTemplate from '../../../../client/views/root/BlazeTemplate'; +import { omit } from '../../../../lib/utils/omit'; +import { RoomSkeleton, RoomProvider, Room, RoomNotFound } from '../../../../client/views/room'; export async function openRoom(type: RoomType, name: string, render = true) { setTimeout(() => { @@ -32,7 +32,7 @@ export async function openRoom(type: RoomType, name: string, render = true) { try { const room = roomCoordinator.getRoomDirectives(type)?.findRoom(name) || (await call('getRoomByTypeAndName', type, name)); - Rooms.upsert({ _id: room._id }, _.omit(room, '_id')); + Rooms.upsert({ _id: room._id }, omit(room, '_id')); if (room._id !== name && type === 'd') { // Redirect old url using username to rid @@ -49,7 +49,11 @@ export async function openRoom(type: RoomType, name: string, render = true) { if (render) { appLayout.render( <MainLayout> - <BlazeTemplate template='room' /> + <Suspense fallback={<RoomSkeleton />}> + <RoomProvider rid={room._id}> + <Room /> + </RoomProvider> + </Suspense> </MainLayout>, ); } @@ -63,7 +67,7 @@ export async function openRoom(type: RoomType, name: string, render = true) { NewRoomManager.open(room._id); Session.set('openedRoom', room._id); - fireGlobalEvent('room-opened', _.omit(room, 'usernames')); + fireGlobalEvent('room-opened', omit(room, 'usernames')); Session.set('editRoomTitle', false); // KonchatNotification.removeRoomNotification(params._id) @@ -111,7 +115,7 @@ export async function openRoom(type: RoomType, name: string, render = true) { Session.set('roomNotFound', { type, name, error }); appLayout.render( <MainLayout> - <BlazeTemplate template='roomNotFound' /> + <RoomNotFound /> </MainLayout>, ); } diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index 18deb29f36f..f7f67f76a70 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -4,7 +4,6 @@ import './lib/iframeCommands'; import './lib/menu'; import './lib/parentTemplate'; import './lib/codeMirror'; -import './views/app/room.ts'; import './views/app/roomSearch.html'; import './views/app/userSearch.html'; import './views/app/roomSearch'; diff --git a/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts b/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts index 8eff97e308a..a5c6f2c9f68 100644 --- a/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts +++ b/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts @@ -1,8 +1,8 @@ import type { ToolboxContextValue } from '../../../../../../client/views/room/lib/Toolbox/ToolboxContext'; -export type CommonRoomTemplateInstance = Blaze.TemplateInstance<{ - rid: string; - tabBar: ToolboxContextValue; -}> & { - tabBar: ToolboxContextValue; +export type CommonRoomTemplateInstance = { + data: { + rid: string; + tabBar: ToolboxContextValue; + }; }; diff --git a/apps/meteor/app/ui/client/views/app/lib/RoomTemplateInstance.ts b/apps/meteor/app/ui/client/views/app/lib/RoomTemplateInstance.ts deleted file mode 100644 index 1b95d738619..00000000000 --- a/apps/meteor/app/ui/client/views/app/lib/RoomTemplateInstance.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import type { Blaze } from 'meteor/blaze'; -import type { ReactiveDict } from 'meteor/reactive-dict'; -import type { ReactiveVar } from 'meteor/reactive-var'; - -import type { FileUploadProp } from '../../../lib/fileUpload'; -import type { CommonRoomTemplateInstance } from './CommonRoomTemplateInstance'; - -export type RoomTemplateInstance = CommonRoomTemplateInstance & - Blaze.TemplateInstance<{ - _id: string; - }> & { - selectable: ReactiveVar<boolean>; - resetSelection: (enabled: boolean) => void; - selectedMessages: unknown[]; - getSelectedMessages: () => unknown[]; - selectMessages: (messages: string) => void; - selectedRange: unknown[]; - selectablePointer: string | undefined; - atBottom: boolean | undefined; - sendToBottomIfNecessary: () => void; - checkIfScrollIsAtBottom: () => void; - observer: ResizeObserver | undefined; - lastScrollTop: number; - hideLeaderHeader: ReactiveVar<boolean>; - isAtBottom: (threshold?: number) => boolean; - sendToBottom: () => void; - state: ReactiveDict<{ - announcement: string; - count: number; - subscribed: boolean; - lastMessage: Date; - autoTranslate: unknown; - autoTranslateLanguage: unknown; - }>; - rid: string; - subscription: ReactiveVar<ISubscription | null>; - room: IRoom | undefined; - unreadCount: ReactiveVar<number>; - rolesObserve: Meteor.LiveQueryHandle | undefined; - onWindowResize: () => void; - onFile: (files: FileUploadProp) => void; - userDetail: ReactiveVar<string>; - }; diff --git a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts index 6c8c0227c94..dc0819106b8 100644 --- a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts +++ b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts @@ -39,8 +39,8 @@ const createMessageTouchEvents = () => { } }, - 'touchstart .message'(e: JQuery.TouchStartEvent, t: CommonRoomTemplateInstance) { - const touches = e.originalEvent?.touches; + 'touchstart .message'(event: JQuery.TouchStartEvent, template: CommonRoomTemplateInstance) { + const touches = event.originalEvent?.touches; if (touches?.length) { lastTouchX = touches[0].pageX; lastTouchY = touches[0].pageY; @@ -50,21 +50,22 @@ const createMessageTouchEvents = () => { return; } - if ($(e.currentTarget).hasClass('system')) { + if ($(event.currentTarget).hasClass('system')) { return; } - if (e.target && e.target.nodeName === 'AUDIO') { + if (event.target && event.target.nodeName === 'AUDIO') { return; } - if (e.target && e.target.nodeName === 'A' && isURL(e.target.getAttribute('href'))) { - e.preventDefault(); - e.stopPropagation(); + if (event.target && event.target.nodeName === 'A' && isURL(event.target.getAttribute('href'))) { + event.preventDefault(); + event.stopPropagation(); } const doLongTouch = () => { - mountPopover(e, t, this); + const data = Blaze.getData(event.currentTarget); + mountPopover(event, template, data); }; clearTimeout(touchtime); @@ -102,33 +103,41 @@ const createMessageTouchEvents = () => { }; }; -function handleMessageActionButtonClick(this: unknown, event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { +function handleMessageActionButtonClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { tabBar } = template.data; const button = MessageAction.getButtonById(event.currentTarget.dataset.messageAction); - // @ ts-ignore - button?.action.call(this, event, { tabbar: template.tabBar }); + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + button?.action.call(dataContext, event, { tabbar: tabBar }); } -function handleFollowThreadButtonClick(this: unknown, e: JQuery.ClickEvent) { - e.preventDefault(); - e.stopPropagation(); - const { msg } = messageArgs(this); +function handleFollowThreadButtonClick(event: JQuery.ClickEvent) { + event.preventDefault(); + event.stopPropagation(); + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + const { msg } = messageArgs(dataContext); callWithErrorHandling('followMessage', { mid: msg._id }); } -function handleUnfollowThreadButtonClick(this: unknown, e: JQuery.ClickEvent) { - e.preventDefault(); - e.stopPropagation(); - const { msg } = messageArgs(this); +function handleUnfollowThreadButtonClick(event: JQuery.ClickEvent) { + event.preventDefault(); + event.stopPropagation(); + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + const { msg } = messageArgs(dataContext); callWithErrorHandling('unfollowMessage', { mid: msg._id }); } -function handleOpenThreadButtonClick(this: unknown, event: JQuery.ClickEvent) { +function handleOpenThreadButtonClick(event: JQuery.ClickEvent) { event.preventDefault(); event.stopPropagation(); + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); const { msg: { rid, _id, tmid }, - } = messageArgs(this); + } = messageArgs(dataContext); const room = Rooms.findOne({ _id: rid }); FlowRouter.go( @@ -147,8 +156,10 @@ function handleOpenThreadButtonClick(this: unknown, event: JQuery.ClickEvent) { ); } -function handleDownloadImageButtonClick(this: unknown, event: JQuery.ClickEvent) { - const { msg } = messageArgs(this); +function handleDownloadImageButtonClick(event: JQuery.ClickEvent) { + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + const { msg } = messageArgs(dataContext); ChatMessage.update({ '_id': msg._id, 'urls.url': $(event.currentTarget).data('url') }, { $set: { 'urls.$.downloadImages': true } }); ChatMessage.update( { '_id': msg._id, 'attachments.image_url': $(event.currentTarget).data('url') }, @@ -156,8 +167,11 @@ function handleDownloadImageButtonClick(this: unknown, event: JQuery.ClickEvent) ); } -function handleOpenUserCardButtonClick(this: unknown, e: JQuery.ClickEvent, instance: CommonRoomTemplateInstance) { - const { msg } = messageArgs(this); +function handleOpenUserCardButtonClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { rid, tabBar } = template.data; + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + const { msg } = messageArgs(dataContext); if (!Meteor.userId()) { return; } @@ -167,18 +181,18 @@ function handleOpenUserCardButtonClick(this: unknown, e: JQuery.ClickEvent, inst if (username) { openUserCard({ username, - rid: instance.data.rid, - target: e.currentTarget, + rid, + target: event.currentTarget, open: (e: MouseEvent) => { e.preventDefault(); - instance.data.tabBar.openUserInfo(username); + tabBar.openUserInfo(username); }, }); } } -function handleRespondWithMessageActionButtonClick(event: JQuery.ClickEvent, instance: CommonRoomTemplateInstance) { - const { rid } = instance.data; +function handleRespondWithMessageActionButtonClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { rid } = template.data; const msg = event.currentTarget.value; if (!msg) { return; @@ -191,25 +205,25 @@ function handleRespondWithMessageActionButtonClick(event: JQuery.ClickEvent, ins } } -function handleRespondWithQuotedMessageActionButtonClick(event: JQuery.ClickEvent, instance: CommonRoomTemplateInstance) { - const { rid } = instance.data; +function handleRespondWithQuotedMessageActionButtonClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { rid } = template.data; const { id: msgId } = event.currentTarget; - const { $input } = chatMessages[rid]; + const { input } = chatMessages[rid]; - if (!msgId) { + if (!msgId || !input) { return; } const message = Messages.findOne({ _id: msgId }); - let messages = $input?.data('reply') || []; + let messages = $(input)?.data('reply') || []; messages = addMessageToList(messages, message); - $input?.focus().data('mention-user', false).data('reply', messages).trigger('dataChange'); + $(input)?.trigger('focus').data('mention-user', false).data('reply', messages).trigger('dataChange'); } -async function handleSendMessageActionButtonClick(event: JQuery.ClickEvent, instance: CommonRoomTemplateInstance) { - const { rid } = instance.data; +async function handleSendMessageActionButtonClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { rid } = template.data; const msg = event.currentTarget.value; let msgObject = { _id: Random.id(), rid, msg } as IMessage; if (!msg) { @@ -226,8 +240,11 @@ async function handleSendMessageActionButtonClick(event: JQuery.ClickEvent, inst await callWithErrorHandling('sendMessage', msgObject); } -function handleMessageActionMenuClick(this: unknown, e: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { - const messageContext = messageArgs(this); +function handleMessageActionMenuClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + const { rid, tabBar } = template.data; + const messageElement = event.target.closest('.message') as HTMLElement; + const dataContext = Blaze.getData(messageElement); + const messageContext = messageArgs(dataContext); const { msg: message, u: user, context: ctx } = messageContext; const room = Rooms.findOne({ _id: message.rid }); const federationContext = isRoomFederated(room) ? 'federated' : ''; @@ -239,7 +256,7 @@ function handleMessageActionMenuClick(this: unknown, e: JQuery.ClickEvent, templ type: 'message-action', id: item.id, modifier: item.color, - action: () => item.action(e, { tabbar: template.tabBar, message, room }), + action: () => item.action(event, { tabbar: tabBar, message, room }), })); const itemsBelowDivider = ['delete-message', 'report-message']; @@ -262,20 +279,20 @@ function handleMessageActionMenuClick(this: unknown, e: JQuery.ClickEvent, templ }, ], instance: template, - rid: template.data.rid, - data: this, + rid, + data: dataContext, type: 'message-action-menu-options', - currentTarget: e.currentTarget, - activeElement: $(e.currentTarget).parents('.message')[0], + currentTarget: event.currentTarget, + activeElement: messageElement, onRendered: () => new Clipboard('.rc-popover__item'), }; popover.open(config); } -function handleMentionLinkClick(e: JQuery.ClickEvent, instance: CommonRoomTemplateInstance) { - e.stopPropagation(); - e.preventDefault(); +function handleMentionLinkClick(event: JQuery.ClickEvent, template: CommonRoomTemplateInstance) { + event.stopPropagation(); + event.preventDefault(); if (!Meteor.userId()) { return; @@ -285,7 +302,7 @@ function handleMentionLinkClick(e: JQuery.ClickEvent, instance: CommonRoomTempla currentTarget: { dataset: { channel, group, username }, }, - } = e; + } = event; if (channel) { if (isLayoutEmbedded()) { @@ -303,13 +320,14 @@ function handleMentionLinkClick(e: JQuery.ClickEvent, instance: CommonRoomTempla } if (username) { + const { rid, tabBar } = template.data; openUserCard({ username, - rid: instance.data.rid, - target: e.currentTarget, + rid, + target: event.currentTarget, open: (e: MouseEvent) => { e.preventDefault(); - instance.data.tabBar.openUserInfo(username); + tabBar.openUserInfo(username); }, }); } diff --git a/apps/meteor/app/ui/client/views/app/lib/mountPopover.ts b/apps/meteor/app/ui/client/views/app/lib/mountPopover.ts index 73ec73e3c52..73896bfbb85 100644 --- a/apps/meteor/app/ui/client/views/app/lib/mountPopover.ts +++ b/apps/meteor/app/ui/client/views/app/lib/mountPopover.ts @@ -1,14 +1,14 @@ import Clipboard from 'clipboard'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import type { Blaze } from 'meteor/blaze'; import { popover, MessageAction } from '../../../../../ui-utils/client'; import { messageArgs } from '../../../../../../client/lib/utils/messageArgs'; import { Rooms } from '../../../../../models/client'; import { t } from '../../../../../utils/client'; +import type { CommonRoomTemplateInstance } from './CommonRoomTemplateInstance'; -export const mountPopover = (e: JQuery.TriggeredEvent, i: Blaze.TemplateInstance, outerContext: unknown) => { - let context = $(e.target).parents('.message').data('context'); +export const mountPopover = (event: JQuery.TriggeredEvent, template: CommonRoomTemplateInstance, outerContext: unknown) => { + let context = $(event.target).parents('.message').data('context'); if (!context) { context = 'message'; } @@ -64,10 +64,10 @@ export const mountPopover = (e: JQuery.TriggeredEvent, i: Blaze.TemplateInstance groups, }, ], - instance: i, - currentTarget: e.currentTarget, + instance: template, + currentTarget: event.currentTarget, data: outerContext, - activeElement: $(e.currentTarget).parents('.message')[0], + activeElement: $(event.currentTarget).parents('.message')[0], onRendered: () => new Clipboard('.rc-popover__item'), }; diff --git a/apps/meteor/app/ui/client/views/app/lib/retentionPolicy.ts b/apps/meteor/app/ui/client/views/app/lib/retentionPolicy.ts deleted file mode 100644 index af18fa92fbe..00000000000 --- a/apps/meteor/app/ui/client/views/app/lib/retentionPolicy.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; -import moment from 'moment'; - -import { settings } from '../../../../../settings/client'; - -function roomHasGlobalPurge(room: IRoom): boolean { - if (!settings.get('RetentionPolicy_Enabled')) { - return false; - } - - switch (room.t) { - case 'c': - return settings.get('RetentionPolicy_AppliesToChannels'); - case 'p': - return settings.get('RetentionPolicy_AppliesToGroups'); - case 'd': - return settings.get('RetentionPolicy_AppliesToDMs'); - } - - return false; -} - -const hasRetentionPolicy = (room: IRoom): room is IRoomWithRetentionPolicy => 'retention' in room; - -function roomHasPurge(room: IRoom): boolean { - if (!room || !settings.get('RetentionPolicy_Enabled')) { - return false; - } - - if (hasRetentionPolicy(room) && room.retention.enabled !== undefined) { - return room.retention.enabled; - } - - return roomHasGlobalPurge(room); -} - -function roomFilesOnly(room: IRoom): boolean { - if (!room) { - return false; - } - - if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { - return room.retention.filesOnly; - } - - return settings.get('RetentionPolicy_FilesOnly'); -} - -function roomExcludePinned(room: IRoom): boolean { - if (!room) { - return false; - } - - if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { - return room.retention.excludePinned; - } - - return settings.get('RetentionPolicy_DoNotPrunePinned'); -} - -function roomMaxAge(room: IRoom): number | undefined { - if (!room) { - return; - } - - if (!roomHasPurge(room)) { - return; - } - - if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { - return room.retention.maxAge; - } - - if (room.t === 'c') { - return settings.get('RetentionPolicy_MaxAge_Channels'); - } - if (room.t === 'p') { - return settings.get('RetentionPolicy_MaxAge_Groups'); - } - if (room.t === 'd') { - return settings.get('RetentionPolicy_MaxAge_DMs'); - } -} - -export const retentionPolicyHelpers = { - hasPurge() { - const { room } = Template.instance() as unknown as { room: IRoom }; - return roomHasPurge(room); - }, - filesOnly() { - const { room } = Template.instance() as unknown as { room: IRoom }; - return roomFilesOnly(room); - }, - excludePinned() { - const { room } = Template.instance() as unknown as { room: IRoom }; - return roomExcludePinned(room); - }, - purgeTimeout() { - const { room } = Template.instance() as unknown as { room: IRoom }; - const maxAge = roomMaxAge(room); - - if (!maxAge) { - return undefined; - } - - moment.relativeTimeThreshold('s', 60); - moment.relativeTimeThreshold('ss', 0); - moment.relativeTimeThreshold('m', 60); - moment.relativeTimeThreshold('h', 24); - moment.relativeTimeThreshold('d', 31); - moment.relativeTimeThreshold('M', 12); - - return moment.duration(maxAge * 1000 * 60 * 60 * 24).humanize(); - }, -} as const; diff --git a/apps/meteor/app/ui/client/views/app/lib/roomEvents.ts b/apps/meteor/app/ui/client/views/app/lib/roomEvents.ts deleted file mode 100644 index 0ea12af6e30..00000000000 --- a/apps/meteor/app/ui/client/views/app/lib/roomEvents.ts +++ /dev/null @@ -1,138 +0,0 @@ -import _ from 'underscore'; -import { Random } from 'meteor/random'; -import { Blaze } from 'meteor/blaze'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; -import type { IRoom, MessageQuoteAttachment } from '@rocket.chat/core-typings'; - -import { ChatMessage, Subscriptions } from '../../../../../models/client'; -import { RoomHistoryManager, RoomManager, readMessage } from '../../../../../ui-utils/client'; -import { messageArgs } from '../../../../../../client/lib/utils/messageArgs'; -import { chatMessages } from '../../../lib/ChatMessages'; -import type { RoomTemplateInstance } from './RoomTemplateInstance'; - -function handleToggleHiddenButtonClick(e: JQuery.ClickEvent) { - const id = e.currentTarget.dataset.message; - document.getElementById(id)?.classList.toggle('message--ignored'); -} - -function handleMessageClick(e: JQuery.ClickEvent, template: RoomTemplateInstance) { - if (template.selectable.get()) { - window.getSelection?.()?.removeAllRanges(); - const data = Blaze.getData(e.currentTarget); - const { - msg: { _id }, - } = messageArgs(data); - - if (!template.selectablePointer) { - template.selectablePointer = _id; - } - - if (!e.shiftKey) { - template.selectedMessages = template.getSelectedMessages(); - template.selectedRange = []; - template.selectablePointer = _id; - } - - template.selectMessages(_id); - - const selectedMessages = $('.messages-box .message.selected').map((_i, message) => message.id); - const removeClass = _.difference(selectedMessages, template.getSelectedMessages()); - const addClass = _.difference(template.getSelectedMessages(), selectedMessages); - removeClass.forEach((message) => $(`.messages-box #${message}`).removeClass('selected')); - addClass.forEach((message) => $(`.messages-box #${message}`).addClass('selected')); - } -} - -function handleJumpToRecentButtonClick(this: { _id: IRoom['_id'] }, e: JQuery.ClickEvent, template: RoomTemplateInstance) { - e.preventDefault(); - template.atBottom = true; - RoomHistoryManager.clear(template?.data?._id); - RoomHistoryManager.getMoreIfIsEmpty(this._id); -} - -function handleGalleryItemLoad(_e: JQuery.TriggeredEvent, template: RoomTemplateInstance) { - template.sendToBottomIfNecessary(); -} - -function handleBlockWrapperRendered(_e: JQuery.TriggeredEvent, template: RoomTemplateInstance) { - template.sendToBottomIfNecessary(); -} - -function handleNewMessageButtonClick(_event: JQuery.ClickEvent, instance: RoomTemplateInstance) { - instance.atBottom = true; - instance.sendToBottomIfNecessary(); - const input = RoomManager.openedRoom ? chatMessages[RoomManager.openedRoom].input : undefined; - input?.focus(); -} - -function handleUploadProgressCloseButtonClick(this: { id: string }, e: JQuery.ClickEvent) { - e.preventDefault(); - Session.set(`uploading-cancel-${this.id}`, true); -} - -function handleMarkAsReadButtonClick(_e: JQuery.ClickEvent, t: RoomTemplateInstance) { - readMessage.readNow(t.data._id); -} - -function handleUnreadBarJumpToButtonClick(_e: JQuery.ClickEvent, t: RoomTemplateInstance) { - const { _id } = t.data; - const room = RoomHistoryManager.getRoom(_id); - let message = room?.firstUnread.get(); - if (!message) { - const subscription = Subscriptions.findOne({ rid: _id }); - message = ChatMessage.find( - { rid: _id, ts: { $gt: subscription != null ? subscription.ls : undefined } }, - { sort: { ts: 1 }, limit: 1 }, - ).fetch()[0]; - } - RoomHistoryManager.getSurroundingMessages(message, 50); -} - -const handleWrapperScroll = _.throttle(function (this: { _id: IRoom['_id'] }, e: JQuery.ScrollEvent, t: RoomTemplateInstance) { - const $roomLeader = $('.room-leader'); - if ($roomLeader.length) { - if (e.target.scrollTop < t.lastScrollTop) { - t.hideLeaderHeader.set(false); - } else if (t.isAtBottom(100) === false && e.target.scrollTop > ($roomLeader.height() ?? 0)) { - t.hideLeaderHeader.set(true); - } - } - t.lastScrollTop = e.target.scrollTop; - const height = e.target.clientHeight; - const isLoading = RoomHistoryManager.isLoading(this._id); - const hasMore = RoomHistoryManager.hasMore(this._id); - const hasMoreNext = RoomHistoryManager.hasMoreNext(this._id); - - if ((isLoading === false && hasMore === true) || hasMoreNext === true) { - if (hasMore === true && t.lastScrollTop <= height / 3) { - RoomHistoryManager.getMore(this._id); - } else if (hasMoreNext === true && Math.ceil(t.lastScrollTop) >= e.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(this._id); - } - } -}, 100); - -function handleTimeClick(this: unknown, e: JQuery.ClickEvent) { - e.preventDefault(); - const { msg } = messageArgs(this); - const repliedMessageId = (msg.attachments?.[0] as MessageQuoteAttachment).message_link?.split('?msg=')[1]; - FlowRouter.go(FlowRouter.current().context.pathname, undefined, { - ...(repliedMessageId && { msg: repliedMessageId }), - hash: Random.id(), - }); -} - -export const roomEvents = { - 'click .toggle-hidden': handleToggleHiddenButtonClick, - 'click .message': handleMessageClick, - 'click .jump-recent button': handleJumpToRecentButtonClick, - 'load .gallery-item': handleGalleryItemLoad, - 'rendered .js-block-wrapper': handleBlockWrapperRendered, - 'click .new-message': handleNewMessageButtonClick, - 'click .upload-progress-close': handleUploadProgressCloseButtonClick, - 'click .unread-bar > button.mark-read': handleMarkAsReadButtonClick, - 'click .unread-bar > button.jump-to': handleUnreadBarJumpToButtonClick, - 'scroll .wrapper': handleWrapperScroll, - 'click .time a': handleTimeClick, -}; diff --git a/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts b/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts deleted file mode 100644 index fe670ccf3db..00000000000 --- a/apps/meteor/app/ui/client/views/app/lib/roomHelpers.ts +++ /dev/null @@ -1,312 +0,0 @@ -import moment from 'moment'; -import { Meteor } from 'meteor/meteor'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; -import type { IMessage, IRoom, MessageTypesValues } from '@rocket.chat/core-typings'; - -import { t, getUserPreference } from '../../../../../utils/client'; -import { ChatMessage, RoomRoles, Users, Rooms } from '../../../../../models/client'; -import { RoomHistoryManager, RoomManager } from '../../../../../ui-utils/client'; -import { settings } from '../../../../../settings/client'; -import { callbacks } from '../../../../../../lib/callbacks'; -import { hasAllPermission, hasRole } from '../../../../../authorization/client'; -import { isLayoutEmbedded } from '../../../../../../client/lib/utils/isLayoutEmbedded'; -import { roomCoordinator } from '../../../../../../client/lib/rooms/roomCoordinator'; -import { chatMessages } from '../../../lib/ChatMessages'; -import type { RoomTemplateInstance } from './RoomTemplateInstance'; - -function tabBar() { - return (Template.instance() as RoomTemplateInstance).tabBar; -} - -function subscribed() { - const { state } = Template.instance() as RoomTemplateInstance; - return state.get('subscribed'); -} - -function messagesHistory() { - const { rid } = Template.instance() as RoomTemplateInstance; - const room: Pick<IRoom, 'sysMes'> = Rooms.findOne(rid, { fields: { sysMes: 1 } }); - const hideSettings = settings.collection.findOne('Hide_System_Messages') || {}; - const settingValues: MessageTypesValues[] = Array.isArray(room?.sysMes) ? room.sysMes : hideSettings.value || []; - const hideMessagesOfType = new Set( - settingValues.reduce( - (array: MessageTypesValues[], value: MessageTypesValues) => [ - ...array, - ...(value === 'mute_unmute' ? (['user-muted', 'user-unmuted'] as const) : ([value] as const)), - ], - [], - ), - ); - const query: Mongo.Query<IMessage> = { - rid, - _hidden: { $ne: true }, - $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], - ...(hideMessagesOfType.size && { t: { $nin: Array.from(hideMessagesOfType.values()) } }), - }; - - const options = { - sort: { - ts: 1, - }, - }; - - return ChatMessage.find(query, options); -} - -function hasMore(this: { _id: string }) { - return RoomHistoryManager.hasMore(this._id); -} - -function hasMoreNext(this: { _id: string }) { - return RoomHistoryManager.hasMoreNext(this._id); -} - -function isLoading(this: { _id: string }) { - return RoomHistoryManager.isLoading(this._id); -} - -function windowId(this: { _id: string }) { - return `chat-window-${this._id}`; -} - -function uploading() { - return Session.get('uploading'); -} - -function roomLeader(this: { _id: string }) { - const roles = RoomRoles.findOne({ - 'rid': this._id, - 'roles': 'leader', - 'u._id': { $ne: Meteor.userId() }, - }); - if (roles) { - const leader = Users.findOne({ _id: roles.u._id }, { fields: { status: 1, statusText: 1 } }) || {}; - - return { - ...roles.u, - name: settings.get('UI_Use_Real_Name') ? roles.u.name || roles.u.username : roles.u.username, - status: leader.status || 'offline', - statusDisplay: leader.statusText || t(leader.status || 'offline'), - }; - } -} - -function chatNowLink(this: { username: string }) { - return roomCoordinator.getRouteLink('d', { name: this.username }); -} - -function announcement() { - return (Template.instance() as RoomTemplateInstance).state.get('announcement'); -} - -function announcementDetails(this: { _id: string }) { - const roomData = Session.get(`roomData${this._id}`); - if (roomData?.announcementDetails?.callback) { - return () => callbacks.run(roomData.announcementDetails.callback, this._id); - } -} - -function messageboxData(this: { _id: string }) { - const { sendToBottomIfNecessary, subscription } = Template.instance() as RoomTemplateInstance; - const { _id: rid } = this; - const isEmbedded = isLayoutEmbedded(); - const showFormattingTips = settings.get('Message_ShowFormattingTips'); - - return { - rid, - subscription: subscription.get(), - isEmbedded, - showFormattingTips: showFormattingTips && !isEmbedded, - onInputChanged: (input: HTMLTextAreaElement) => { - if (!chatMessages[rid]) { - return; - } - - chatMessages[rid].initializeInput(input, { rid }); - }, - onResize: () => sendToBottomIfNecessary?.(), - onKeyUp: ( - event: KeyboardEvent, - { - rid, - tmid, - }: { - rid: string; - tmid?: string | undefined; - }, - ) => chatMessages[rid]?.keyup(event, { rid, tmid }), - onKeyDown: (event: KeyboardEvent) => chatMessages[rid]?.keydown(event), - onSend: ( - event: Event, - params: { - rid: string; - tmid?: string; - value: string; - tshow?: boolean; - }, - done?: () => void, - ) => chatMessages[rid]?.send(event, params, done), - }; -} - -function getAnnouncementStyle() { - const { room } = Template.instance() as RoomTemplateInstance; - return room?.announcementDetails?.style ?? ''; -} - -function maxMessageLength() { - return settings.get('Message_MaxAllowedSize'); -} - -function subscriptionReady(this: { _id: string }) { - return RoomManager.getOpenedRoomByRid(this._id)?.streamActive ?? false; -} - -type UnreadData = { count?: number; since?: moment.MomentInput }; - -function unreadData(this: { _id: string }) { - const data: UnreadData = { count: (Template.instance() as RoomTemplateInstance).state.get('count'), since: undefined }; - - const room = RoomManager.getOpenedRoomByRid(this._id); - if (room) { - data.since = room.unreadSince.get(); - } - - return data; -} - -function containerBarsShow(unreadData: UnreadData, uploading: unknown[]) { - const hasUnreadData = Boolean(unreadData?.count && unreadData.since); - const isUploading = Boolean(uploading?.length); - - if (hasUnreadData || isUploading) { - return 'show'; - } -} - -function formatUnreadSince(this: UnreadData) { - if (!this.since) { - return; - } - - return moment(this.since).calendar(null, { sameDay: 'LT' }); -} - -function adminClass() { - const uid = Meteor.userId(); - if (uid && hasRole(uid, 'admin')) { - return 'admin'; - } -} - -function messageViewMode() { - const modes = ['', 'cozy', 'compact'] as const; - const viewMode = getUserPreference(Meteor.userId(), 'messageViewMode') as keyof typeof modes; - return modes[viewMode] || modes[0]; -} - -function selectable() { - return (Template.instance() as RoomTemplateInstance).selectable.get(); -} - -function hideUsername() { - return getUserPreference(Meteor.userId(), 'hideUsernames') ? 'hide-usernames' : undefined; -} - -function hideAvatar() { - return getUserPreference(Meteor.userId(), 'displayAvatars') ? undefined : 'hide-avatars'; -} - -function canPreview() { - const { room, state } = Template.instance() as RoomTemplateInstance; - - if (room && room.t !== 'c') { - return true; - } - - if (settings.get('Accounts_AllowAnonymousRead') === true) { - return true; - } - - if (hasAllPermission('preview-c-room')) { - return true; - } - - return state.get('subscribed'); -} - -function hideLeaderHeader() { - return (Template.instance() as RoomTemplateInstance).hideLeaderHeader.get() ? 'animated-hidden' : ''; -} - -function hasLeader(this: { _id: string }) { - if (RoomRoles.findOne({ 'rid': this._id, 'roles': 'leader', 'u._id': { $ne: Meteor.userId() } }, { fields: { _id: 1 } })) { - return 'has-leader'; - } -} - -function openedThread() { - FlowRouter.watchPathChange(); - const tab = FlowRouter.getParam('tab'); - const mid = FlowRouter.getParam('context'); - const rid = Template.currentData()._id; - const jump = FlowRouter.getQueryParam('jump'); - const subscription = (Template.instance() as RoomTemplateInstance).subscription.get(); - - if (tab !== 'thread' || !mid || rid !== Session.get('openedRoom')) { - return; - } - - const room = Rooms.findOne( - { _id: rid }, - { - fields: { - t: 1, - usernames: 1, - uids: 1, - name: 1, - }, - }, - ); - - return { - rid, - mid, - room, - jump, - subscription, - }; -} - -export const roomHelpers = { - tabBar, - subscribed, - messagesHistory, - hasMore, - hasMoreNext, - isLoading, - windowId, - uploading, - roomLeader, - chatNowLink, - announcement, - announcementDetails, - messageboxData, - getAnnouncementStyle, - maxMessageLength, - subscriptionReady, - unreadData, - containerBarsShow, - formatUnreadSince, - adminClass, - messageViewMode, - selectable, - hideUsername, - hideAvatar, - canPreview, - hideLeaderHeader, - hasLeader, - openedThread, -}; diff --git a/apps/meteor/app/ui/client/views/app/lib/roomLifeCycleMethods.ts b/apps/meteor/app/ui/client/views/app/lib/roomLifeCycleMethods.ts deleted file mode 100644 index e2ebe3a748c..00000000000 --- a/apps/meteor/app/ui/client/views/app/lib/roomLifeCycleMethods.ts +++ /dev/null @@ -1,428 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { ReactiveDict } from 'meteor/reactive-dict'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; -import type { IEditedMessage, IMessage, IRoom } from '@rocket.chat/core-typings'; - -import { ChatMessage, RoomRoles, Subscriptions, Rooms } from '../../../../../models/client'; -import { RoomHistoryManager, RoomManager, readMessage } from '../../../../../ui-utils/client'; -import { callbacks } from '../../../../../../lib/callbacks'; -import { ChatMessages, chatMessages } from '../../../lib/ChatMessages'; -import { fileUpload } from '../../../lib/fileUpload'; -import { RoomManager as NewRoomManager } from '../../../../../../client/lib/RoomManager'; -import { roomCoordinator } from '../../../../../../client/lib/rooms/roomCoordinator'; -import { queryClient } from '../../../../../../client/lib/queryClient'; -import { call } from '../../../../../../client/lib/utils/call'; -import { isAtBottom } from './scrolling'; -import { dispatchToastMessage } from '../../../../../../client/lib/toast'; -import type { RoomTemplateInstance } from './RoomTemplateInstance'; - -export function onRoomCreated(this: RoomTemplateInstance) { - // this.scrollOnBottom = true - // this.typing = new msgTyping this.data._id - const rid = this.data._id; - this.tabBar = this.data.tabBar; - - this.onFile = (filesToUpload) => { - const { input } = chatMessages[rid]; - if (!input) return; - - fileUpload(filesToUpload, input, { rid }); - }; - - this.rid = rid; - - this.subscription = new ReactiveVar(null); - this.state = new ReactiveDict(); - this.userDetail = new ReactiveVar(''); - const user = Meteor.user(); - this.autorun((c) => { - const room: IRoom = Rooms.findOne( - { _id: rid }, - { - fields: { - t: 1, - usernames: 1, - uids: 1, - }, - }, - ); - - if (room.t !== 'd') { - return c.stop(); - } - - if (roomCoordinator.getRoomDirectives(room.t)?.isGroupChat(room)) { - return; - } - const usernames = Array.from(new Set(room.usernames)); - this.userDetail.set( - this.userDetail.get() || (usernames.length === 1 ? usernames[0] : usernames.filter((username) => username !== user?.username)[0]), - ); - }); - - this.autorun(() => { - const rid = Template.currentData()._id; - const room = Rooms.findOne({ _id: rid }, { fields: { announcement: 1 } }); - this.state.set('announcement', room.announcement); - }); - - this.autorun(() => { - const subscription = Subscriptions.findOne({ rid }); - this.subscription.set(subscription); - this.state.set({ - subscribed: !!subscription, - autoTranslate: subscription?.autoTranslate, - autoTranslateLanguage: subscription?.autoTranslateLanguage, - }); - }); - - this.atBottom = !FlowRouter.getQueryParam('msg'); - this.unreadCount = new ReactiveVar(0); - - this.selectable = new ReactiveVar(false); - this.selectedMessages = []; - this.selectedRange = []; - this.selectablePointer = undefined; - - this.hideLeaderHeader = new ReactiveVar(false); - - this.resetSelection = (enabled: boolean) => { - this.selectable.set(enabled); - $('.messages-box .message.selected').removeClass('selected'); - this.selectedMessages = []; - this.selectedRange = []; - this.selectablePointer = undefined; - }; - - this.selectMessages = (to) => { - if (this.selectablePointer === to && this.selectedRange.length > 0) { - this.selectedRange = []; - } else { - const message1 = ChatMessage.findOne(this.selectablePointer); - const message2 = ChatMessage.findOne(to); - - if (!message1 || !message2) { - throw new Error('Invalid message selection'); - } - - const minTs = _.min([message1.ts, message2.ts]) as Date; - const maxTs = _.max([message1.ts, message2.ts]) as Date; - - this.selectedRange = _.pluck(ChatMessage.find({ rid: message1.rid, ts: { $gte: minTs, $lte: maxTs } }).fetch(), '_id'); - } - }; - - this.getSelectedMessages = () => { - let previewMessages; - const messages = this.selectedMessages; - let addMessages = false; - for (const message of Array.from(this.selectedRange)) { - if (messages.indexOf(message) === -1) { - addMessages = true; - break; - } - } - - if (addMessages) { - previewMessages = _.compact(_.uniq(this.selectedMessages.concat(this.selectedRange))); - } else { - previewMessages = _.compact(_.difference(this.selectedMessages, this.selectedRange)); - } - - return previewMessages; - }; - - queryClient - .fetchQuery({ - queryKey: ['room', this.data._id, 'roles'], - queryFn: () => call('getRoomRoles', this.data._id), - staleTime: 15_000, - }) - .then((results) => { - Array.from(results).forEach(({ _id, ...data }) => { - const { - rid, - u: { _id: uid }, - } = data; - RoomRoles.upsert({ rid, 'u._id': uid }, data); - }); - }) - .catch((error) => { - dispatchToastMessage({ type: 'error', message: error }); - }); - - this.rolesObserve = RoomRoles.find({ rid: this.data._id }).observe({ - added: (role) => { - if (!role.u || !role.u._id) { - return; - } - ChatMessage.update({ 'rid': this.data._id, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - }, // Update message to re-render DOM - changed: (role) => { - if (!role.u || !role.u._id) { - return; - } - ChatMessage.update({ 'rid': this.data._id, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); - }, // Update message to re-render DOM - removed: (role) => { - if (!role.u || !role.u._id) { - return; - } - ChatMessage.update({ 'rid': this.data._id, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - }, - }); - - this.sendToBottomIfNecessary = () => { - if (this.atBottom === true) { - this.sendToBottom(); - } - }; -} - -export function onRoomDestroyed(this: RoomTemplateInstance) { - if (this.rolesObserve) { - this.rolesObserve.stop(); - } - - // @ts-ignore - readMessage.off(this.data._id); - - window.removeEventListener('resize', this.onWindowResize); - - this.observer?.disconnect(); - - const chatMessage = chatMessages[this.data._id]; - chatMessage.onDestroyed?.(this.data._id); - - callbacks.remove('streamNewMessage', this.data._id); -} - -export function onRoomRendered(this: RoomTemplateInstance) { - const { _id: rid } = this.data; - - if (!chatMessages[rid]) { - chatMessages[rid] = new ChatMessages(); - } - - const wrapper = this.find('.wrapper'); - - const store = NewRoomManager.getStore(rid); - - const afterMessageGroup = () => { - if (store?.scroll && !store.atBottom) { - wrapper.scrollTop = store.scroll; - } else { - this.sendToBottom(); - } - wrapper.removeEventListener('MessageGroup', afterMessageGroup); - - wrapper.addEventListener( - 'scroll', - _.throttle(() => { - store?.update({ scroll: wrapper.scrollTop, atBottom: isAtBottom(wrapper, 50) }); - }, 30), - ); - }; - - wrapper.addEventListener('MessageGroup', afterMessageGroup); - - chatMessages[rid].initializeWrapper(this.find('.wrapper')); - chatMessages[rid].initializeInput(this.find('.js-input-message') as HTMLTextAreaElement, { rid }); - - const wrapperUl = this.find('.wrapper > ul'); - const newMessage = this.find('.new-message'); - - const messageBox = $('.messages-box'); - - this.isAtBottom = function (scrollThreshold = 0) { - if (isAtBottom(wrapper, scrollThreshold)) { - newMessage.className = 'new-message background-primary-action-color color-content-background-color not'; - return true; - } - return false; - }; - - this.sendToBottom = function () { - wrapper.scrollTo(30, wrapper.scrollHeight); - newMessage.className = 'new-message background-primary-action-color color-content-background-color not'; - }; - - this.checkIfScrollIsAtBottom = () => { - this.atBottom = this.isAtBottom(100); - }; - - this.observer = new ResizeObserver(() => this.sendToBottomIfNecessary()); - - this.observer.observe(wrapperUl); - - const wheelHandler = _.throttle(() => { - this.checkIfScrollIsAtBottom(); - }, 100); - - wrapper.addEventListener('mousewheel', wheelHandler); - - wrapper.addEventListener('wheel', wheelHandler); - - wrapper.addEventListener('touchstart', () => { - this.atBottom = false; - }); - - wrapper.addEventListener('touchend', () => { - this.checkIfScrollIsAtBottom(); - setTimeout(() => this.checkIfScrollIsAtBottom(), 1000); - setTimeout(() => this.checkIfScrollIsAtBottom(), 2000); - }); - - Tracker.afterFlush(() => { - wrapper.addEventListener('scroll', wheelHandler); - }); - - this.lastScrollTop = $('.messages-box .wrapper').scrollTop() ?? 0; - - const rtl = $('html').hasClass('rtl'); - - const getElementFromPoint = function (topOffset = 0) { - const messageBoxOffset = messageBox.offset(); - const messageBoxWidth = messageBox.width(); - - if (messageBoxOffset === undefined || messageBoxWidth === undefined) { - return undefined; - } - - let element; - if (rtl) { - element = document.elementFromPoint(messageBoxOffset.left + messageBoxWidth - 1, messageBoxOffset.top + topOffset + 1); - } else { - element = document.elementFromPoint(messageBoxOffset.left + 1, messageBoxOffset.top + topOffset + 1); - } - - if (element?.classList.contains('message')) { - return element; - } - }; - - const updateUnreadCount = _.throttle(() => { - Tracker.afterFlush(() => { - const lastInvisibleMessageOnScreen = getElementFromPoint(0) || getElementFromPoint(20) || getElementFromPoint(40); - - if (!lastInvisibleMessageOnScreen || !lastInvisibleMessageOnScreen.id) { - return this.unreadCount.set(0); - } - - const lastMessage = ChatMessage.findOne(lastInvisibleMessageOnScreen.id); - if (!lastMessage) { - return this.unreadCount.set(0); - } - - this.state.set('lastMessage', lastMessage.ts); - }); - }, 300); - - const read = _.debounce(function () { - if (rid !== Session.get('openedRoom')) { - return; - } - readMessage.read(rid); - }, 500); - - this.autorun(() => { - Tracker.afterFlush(() => { - if (rid !== Session.get('openedRoom')) { - return; - } - - let room = Rooms.findOne({ _id: rid }, { fields: { t: 1 } }); - - if (room?.t === 'l') { - room = Tracker.nonreactive(() => Rooms.findOne({ _id: rid })); - roomCoordinator.getRoomDirectives(room.t)?.openCustomProfileTab(this, room, room.v.username); - } - }); - }); - - this.autorun(() => { - if (!roomCoordinator.isRouteNameKnown(FlowRouter.getRouteName())) { - return; - } - - if (rid !== Session.get('openedRoom')) { - return; - } - - const subscription = Subscriptions.findOne({ rid }, { fields: { alert: 1, unread: 1 } }); - read(); - return subscription && (subscription.alert || subscription.unread) && readMessage.refreshUnreadMark(rid); - }); - - this.autorun(() => { - const lastMessage = this.state.get('lastMessage'); - - const subscription = Subscriptions.findOne({ rid }, { fields: { ls: 1 } }); - if (!subscription) { - this.unreadCount.set(0); - return; - } - - const count = ChatMessage.find({ - rid, - ts: { $lte: lastMessage, $gt: subscription?.ls }, - }).count(); - - this.unreadCount.set(count); - }); - - this.autorun(() => { - const count = RoomHistoryManager.getRoom(rid).unreadNotLoaded.get() + this.unreadCount.get(); - this.state.set('count', count); - }); - - this.autorun(() => { - Rooms.findOne(rid); - const count = this.state.get('count'); - if (count === 0) { - return read(); - } - readMessage.refreshUnreadMark(rid); - }); - - readMessage.on(this.data._id, () => this.unreadCount.set(0)); - - wrapper.addEventListener('scroll', updateUnreadCount); - // save the render's date to display new messages alerts - $.data(this.firstNode, 'renderedAt', new Date()); - - callbacks.add( - 'streamNewMessage', - (msg: IMessage) => { - if (rid !== msg.rid || (msg as IEditedMessage).editedAt || msg.tmid) { - return; - } - - if (msg.u._id === Meteor.userId()) { - return this.sendToBottom(); - } - - if (!this.isAtBottom()) { - newMessage.classList.remove('not'); - } - }, - callbacks.priority.MEDIUM, - rid, - ); - - this.autorun(() => { - if (this.data._id !== RoomManager.openedRoom) { - return; - } - - const room = Rooms.findOne({ _id: this.data._id }); - if (!room) { - return FlowRouter.go('home'); - } - }); -} diff --git a/apps/meteor/app/ui/client/views/app/room.html b/apps/meteor/app/ui/client/views/app/room.html deleted file mode 100644 index 97b403e4ee9..00000000000 --- a/apps/meteor/app/ui/client/views/app/room.html +++ /dev/null @@ -1,144 +0,0 @@ -<template name="roomOld"> - <div class="main-content-flex"> - <section class="messages-container flex-tab-main-content {{adminClass}}" id="{{windowId}}" aria-label="{{_ "Channel"}}"> - <div class="messages-container-wrapper"> - <div class="messages-container-main dropzone {{dragAndDrop}}"> - <div class="dropzone-overlay {{isDropzoneDisabled}} background-transparent-darkest color-content-background-color">{{_ dragAndDropLabel}}</div> - <div class="container-bars {{containerBarsShow unreadData uploading}}"> - {{#with unreadData}} - {{#if since}} - {{#if count}} - <div class="unread-bar color-primary-action-color background-component-color"> - <button class="jump-to"> - <span class="jump-to-large">{{_ "Jump_to_first_unread"}}</span> - <span class="jump-to-small">{{_ "Jump"}}</span> - </button> - <span class="unread-count-since"> - {{_ "S_new_messages_since_s" count formatUnreadSince}} - </span> - <span class="unread-count"> - {{_ "N_new_messages" count}} - </span> - <button class="mark-read"> - {{_ "Mark_as_read"}} - </button> - </div> - {{/if}} - {{/if}} - {{/with}} - {{#each uploading}} - <div class="upload-progress color-primary-action-color background-component-color {{#if error}}error-background error-border{{/if}}"> - {{#if error}} - <div class="upload-progress-text"> - {{error}} - </div> - <button class="upload-progress-close"> - {{_ "close"}} - </button> - {{else}} - <div class="upload-progress-progress" style="width: {{percentage}}%;"></div> - <div class="upload-progress-text"> - [{{percentage}}%] {{name}} - </div> - <button class="upload-progress-close"> - {{_ "Cancel"}} - </button> - {{/if}} - </div> - {{/each}} - </div> - <div class="messages-box {{#if selectable}}selectable{{/if}} {{messageViewMode}} {{hasLeader}}"> - <div class="ticks-bar"></div> - <button class="new-message background-primary-action-color color-content-background-color not"> - <i class="icon-down-big"></i> - {{_ "New_messages"}} - </button> - <div class="jump-recent background-component-color {{#unless hasMoreNext}}not{{/unless}}"> - <button>{{_ "Jump_to_recent_messages"}} <i class="icon-level-down"></i></button> - </div> - {{#unless canPreview}} - <div class="content room-not-found error-color"> - <div> - {{_ "You_must_join_to_view_messages_in_this_channel"}} - </div> - </div> - {{/unless}} - {{#with roomLeader}} - <div class="room-leader message color-primary-font-color content-background-color border-component-color {{hideLeaderHeader}}"> - <button class="thumb user-card-message"> - {{> avatar username=username }} - </button> - <div class="leader-name">{{name}}</div> - <div class="leader-status userStatus"> - <span class="color-ball status-bg-{{status}}"></span> - <span class="status-text leader-status-text">{{statusDisplay}}</span> - </div> - <a class="chat-now" href="{{chatNowLink}}">{{_ "Chat_Now"}}</a> - </div> - {{/with}} - <div class="wrapper {{#if hasMoreNext}}has-more-next{{/if}} {{hideUsername}} {{hideAvatar}}"> - <ul class="messages-list" aria-live="polite"> - {{#if canPreview}} - {{#if hasMore}} - <li class="load-more"> - {{#if isLoading}} - {{> loading}} - {{/if}} - </li> - {{else}} - <li class="start color-info-font-color"> - {{#if hasPurge}} - <div class="start__purge-warning error-background error-border error-color"> - {{> icon block="start__purge-warning-icon" icon="warning"}} - {{#unless filesOnly}} - {{#unless excludePinned}} - {{_ "RetentionPolicy_RoomWarning" time=purgeTimeout}} - {{else}} - {{_ "RetentionPolicy_RoomWarning_Unpinned" time=purgeTimeout}} - {{/unless}} - {{else}} - {{#unless excludePinned}} - {{_ "RetentionPolicy_RoomWarning_FilesOnly" time=purgeTimeout}} - {{else}} - {{_ "RetentionPolicy_RoomWarning_UnpinnedFilesOnly" time=purgeTimeout}} - {{/unless}} - {{/unless}} - </div> - {{/if}} - - {{> RoomForeword }} - </li> - {{/if}} - {{/if}} - {{#unless $eq (preference 'useLegacyMessageTemplate') true}} - {{> MessageList rid=rid }} - {{else}} - {{#with messageContext}} - {{#each msg in messagesHistory}} - {{> message showRoles=true index=@index shouldCollapseReplies=false msg=msg room=room actions=actions subscription=subscription settings=settings u=u}} - {{/each}} - {{/with}} - {{/unless}} - - {{#if hasMoreNext}} - <li class="load-more"> - {{#if isLoading}} - {{> loading}} - {{/if}} - </li> - {{/if}} - </ul> - </div> - </div> - <footer class="footer border-component-color"> - {{#if subscriptionReady}} - {{> messageBox messageboxData}} - {{else}} - {{> ComposerSkeleton}} - {{/if}} - </footer> - </div> - </div> - </section> - </div> -</template> diff --git a/apps/meteor/app/ui/client/views/app/room.ts b/apps/meteor/app/ui/client/views/app/room.ts deleted file mode 100644 index c4a26e01681..00000000000 --- a/apps/meteor/app/ui/client/views/app/room.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Template } from 'meteor/templating'; - -import { messageContext } from '../../../../ui-utils/client/lib/messageContext'; -import { getCommonRoomEvents } from './lib/getCommonRoomEvents'; -import { dropzoneHelpers, dropzoneEvents } from './lib/dropzone'; -import { retentionPolicyHelpers } from './lib/retentionPolicy'; -import { roomEvents } from './lib/roomEvents'; -import { roomHelpers } from './lib/roomHelpers'; -import { onRoomCreated, onRoomDestroyed, onRoomRendered } from './lib/roomLifeCycleMethods'; -import './room.html'; - -Template.roomOld.helpers({ - ...dropzoneHelpers, - ...roomHelpers, - ...retentionPolicyHelpers, - messageContext, -}); - -Template.roomOld.events({ - ...getCommonRoomEvents(), - ...dropzoneEvents, - ...roomEvents, -}); - -Template.roomOld.onCreated(onRoomCreated); -Template.roomOld.onDestroyed(onRoomDestroyed); -Template.roomOld.onRendered(onRoomRendered); diff --git a/apps/meteor/client/hooks/useFormatRelativeTime.ts b/apps/meteor/client/hooks/useFormatRelativeTime.ts new file mode 100644 index 00000000000..5f0a0675cf6 --- /dev/null +++ b/apps/meteor/client/hooks/useFormatRelativeTime.ts @@ -0,0 +1,14 @@ +import moment from 'moment'; +import { useCallback } from 'react'; + +export const useFormatRelativeTime = (): ((timeMs: number) => string) => + useCallback((timeMs: number) => { + moment.relativeTimeThreshold('s', 60); + moment.relativeTimeThreshold('ss', 0); + moment.relativeTimeThreshold('m', 60); + moment.relativeTimeThreshold('h', 24); + moment.relativeTimeThreshold('d', 31); + moment.relativeTimeThreshold('M', 12); + + return moment.duration(timeMs).humanize(); + }, []); diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index fd42fbdd590..2d10424951f 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -3,7 +3,6 @@ import { Accounts } from 'meteor/accounts-base'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import React, { lazy } from 'react'; @@ -14,6 +13,8 @@ import { dispatchToastMessage } from '../lib/toast'; import BlazeTemplate from '../views/root/BlazeTemplate'; import MainLayout from '../views/root/MainLayout'; +const PageLoading = lazy(() => import('../views/root/PageLoading')); +const HomePage = lazy(() => import('../views/home/HomePage')); const InvitePage = lazy(() => import('../views/invite/InvitePage')); const SecretURLPage = lazy(() => import('../views/invite/SecretURLPage')); const CMSPage = lazy(() => import('../views/root/CMSPage')); @@ -33,7 +34,7 @@ FlowRouter.route('/', { action() { appLayout.render( <MainLayout> - <BlazeTemplate template='loading' /> + <PageLoading /> </MainLayout>, ); @@ -108,7 +109,7 @@ FlowRouter.route('/home', { appLayout.render( <MainLayout> - <BlazeTemplate template={'HomePage'} /> + <HomePage /> </MainLayout>, ); }); @@ -118,7 +119,7 @@ FlowRouter.route('/home', { appLayout.render( <MainLayout> - <BlazeTemplate template={'HomePage'} /> + <HomePage /> </MainLayout>, ); }, @@ -178,18 +179,6 @@ FlowRouter.route('/legal-notice', { }, }); -FlowRouter.route('/room-not-found/:type/:name', { - name: 'room-not-found', - action: ({ type, name } = {}) => { - Session.set('roomNotFound', { type, name }); - appLayout.render( - <MainLayout> - <BlazeTemplate template='roomNotFound' /> - </MainLayout>, - ); - }, -}); - FlowRouter.route('/register/:hash', { name: 'register-secret-url', action: () => { diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 478bff9c057..c645b7ebf2a 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -2,10 +2,6 @@ import { HTML } from 'meteor/htmljs'; import { createTemplateForComponent } from './lib/portals/createTemplateForComponent'; -createTemplateForComponent('HomePage', () => import('./views/home/HomePage'), { - attachment: 'at-parent', -}); - createTemplateForComponent('MessageActions', () => import('./components/message/MessageActions')); createTemplateForComponent('reactAttachments', () => import('./components/message/Attachments')); @@ -24,8 +20,6 @@ createTemplateForComponent('DiscussionMetric', () => import('./components/messag }), }); -createTemplateForComponent('MessageList', () => import('./views/room/MessageList/MessageList')); - createTemplateForComponent('BroadCastMetric', () => import('./components/message/Metrics/Broadcast')); createTemplateForComponent( @@ -39,7 +33,11 @@ createTemplateForComponent( }, ); -createTemplateForComponent('RoomForeword', () => import('./components/RoomForeword'), { +createTemplateForComponent('UnreadMessagesIndicator', () => import('./views/room/components/body/UnreadMessagesIndicator'), { + attachment: 'at-parent', +}); + +createTemplateForComponent('UploadProgressIndicator', () => import('./views/room/components/body/UploadProgressIndicator'), { attachment: 'at-parent', }); @@ -58,62 +56,6 @@ createTemplateForComponent('omnichannelFlex', () => import('./views/omnichannel/ renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), }); -createTemplateForComponent('DiscussionMessageList', () => import('./views/room/contextualBar/Discussions'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('ThreadsList', () => import('./views/room/contextualBar/Threads'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('ExportMessages', () => import('./views/room/contextualBar/ExportMessages'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('KeyboardShortcuts', () => import('./views/room/contextualBar/KeyboardShortcuts'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('room', () => import('./views/room/Room'), { - renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), -}); - -createTemplateForComponent('AutoTranslate', () => import('./views/room/contextualBar/AutoTranslate'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('NotificationsPreferences', () => import('./views/room/contextualBar/NotificationPreferences'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('membersList', () => import('./views/room/contextualBar/RoomMembers'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('OTR', () => import('./views/room/contextualBar/OTR'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('EditRoomInfo', () => import('./views/room/contextualBar/Info/EditRoomInfo'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('RoomInfo', () => import('./views/room/contextualBar/Info/RoomInfo'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('UserInfoWithData', () => import('./views/room/contextualBar/UserInfo'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('channelFilesList', () => import('./views/room/contextualBar/RoomFiles'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - -createTemplateForComponent('PruneMessages', () => import('./views/room/contextualBar/PruneMessages'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), -}); - createTemplateForComponent('loginLayoutHeader', () => import('./views/login/LoginLayout/Header')); createTemplateForComponent('loginLayoutFooter', () => import('./views/login/LoginLayout/Footer')); @@ -132,24 +74,8 @@ createTemplateForComponent('accountFlex', () => import('./views/account/AccountS renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), }); -createTemplateForComponent('SortList', () => import('./components/SortList')); - -createTemplateForComponent('CreateRoomList', () => import('./sidebar/header/actions/CreateRoomList')); - -createTemplateForComponent('UserDropdown', () => import('./sidebar/header/UserDropdown')); - createTemplateForComponent('sidebarFooter', () => import('./sidebar/footer')); -createTemplateForComponent('roomNotFound', () => import('./views/room/Room/RoomNotFound'), { - renderContainerView: () => HTML.DIV({ style: 'height: 100%;' }), -}); - -createTemplateForComponent('ComposerNotAvailablePhoneCalls', () => import('./components/voip/composer/NotAvailableOnCall'), { - renderContainerView: () => HTML.DIV({ style: 'display: flex; height: 100%; width: 100%' }), -}); - createTemplateForComponent('loggedOutBanner', () => import('../ee/client/components/deviceManagement/LoggedOutBanner'), { renderContainerView: () => HTML.DIV({ style: 'max-width: 520px; margin: 0 auto;' }), }); - -createTemplateForComponent('ComposerSkeleton', () => import('./views/room/Room/ComposerSkeleton')); diff --git a/apps/meteor/client/views/omnichannel/directory/calls/Call.tsx b/apps/meteor/client/views/omnichannel/directory/calls/Call.tsx index 4c89c969eed..50c2efcebfa 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/Call.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/Call.tsx @@ -1,19 +1,25 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import React, { useEffect, FC } from 'react'; +import React, { useEffect, ReactElement, Suspense } from 'react'; import { openRoom } from '../../../../../app/ui-utils/client/lib/openRoom'; -import RoomWithData from '../../../room/Room'; +import { Room, RoomProvider, RoomSkeleton } from '../../../room'; -const Chat: FC<{ rid: IRoom['_id'] }> = ({ rid }) => { +type CallProps = { rid: IRoom['_id'] }; + +const Call = ({ rid }: CallProps): ReactElement => { useEffect(() => { openRoom('v', rid, false); }, [rid]); return ( <Box position='absolute' backgroundColor='surface' width='full' height='full'> - <RoomWithData /> + <Suspense fallback={<RoomSkeleton />}> + <RoomProvider rid={rid}> + <Room /> + </RoomProvider> + </Suspense> </Box> ); }; -export default Chat; +export default Call; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/Chat.tsx b/apps/meteor/client/views/omnichannel/directory/chats/Chat.tsx index d9100344e3a..72e82ec93d1 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/Chat.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/Chat.tsx @@ -1,20 +1,24 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import React, { useEffect, FC } from 'react'; +import React, { useEffect, Suspense, ReactElement } from 'react'; import { openRoom } from '../../../../../app/ui-utils/client/lib/openRoom'; -import RoomWithData from '../../../room/Room'; +import { Room, RoomProvider, RoomSkeleton } from '../../../room'; -const Chat: FC<{ rid: IRoom['_id'] }> = ({ rid }) => { +type ChatProps = { rid: IRoom['_id'] }; + +const Chat = ({ rid }: ChatProps): ReactElement => { useEffect(() => { - // NewRoomManager.open(rid); - // RoomManager.open(`l${rid}`); openRoom('l', rid, false); }, [rid]); return ( <Box position='absolute' backgroundColor='surface' width='full' height='full'> - <RoomWithData /> + <Suspense fallback={<RoomSkeleton />}> + <RoomProvider rid={rid}> + <Room /> + </RoomProvider> + </Suspense> </Box> ); }; diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index 1cad8505768..fc321bdbc27 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -1,7 +1,7 @@ import { isThreadMessage, IThreadMessage, IRoom } from '@rocket.chat/core-typings'; import { MessageDivider } from '@rocket.chat/fuselage'; import { useUserSubscription, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { FC, Fragment, memo } from 'react'; +import React, { Fragment, memo, ReactElement } from 'react'; import { MessageTypes } from '../../../../app/ui-utils/client'; import { useFormatDate } from '../../../hooks/useFormatDate'; @@ -19,7 +19,11 @@ import { isOwnUserMessage } from './lib/isOwnUserMessage'; import MessageHighlightProvider from './providers/MessageHighlightProvider'; import { MessageListProvider } from './providers/MessageListProvider'; -export const MessageList: FC<{ rid: IRoom['_id'] }> = ({ rid }) => { +type MessageListProps = { + rid: IRoom['_id']; +}; + +export const MessageList = ({ rid }: MessageListProps): ReactElement => { const t = useTranslation(); const messages = useMessages({ rid }); const subscription = useUserSubscription(rid); diff --git a/apps/meteor/client/views/room/Room.stories.tsx b/apps/meteor/client/views/room/Room.stories.tsx deleted file mode 100644 index 1fc5990c9f6..00000000000 --- a/apps/meteor/client/views/room/Room.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import { RoomTemplate } from './components/RoomTemplate/RoomTemplate'; - -export default { - title: 'Room/RoomTemplate', - component: RoomTemplate, -} as ComponentMeta<typeof RoomTemplate>; - -export const Default: ComponentStory<typeof RoomTemplate> = () => ( - <RoomTemplate> - <RoomTemplate.Header>header</RoomTemplate.Header> - <RoomTemplate.Body>body</RoomTemplate.Body> - <RoomTemplate.Footer>footer</RoomTemplate.Footer> - <RoomTemplate.Aside>Aside</RoomTemplate.Aside> - </RoomTemplate> -); -Default.storyName = 'RoomTemplate'; diff --git a/apps/meteor/client/views/room/Room/ComposerSkeleton.tsx b/apps/meteor/client/views/room/Room/ComposerSkeleton.tsx index e86df560df0..58855ead2fc 100644 --- a/apps/meteor/client/views/room/Room/ComposerSkeleton.tsx +++ b/apps/meteor/client/views/room/Room/ComposerSkeleton.tsx @@ -2,8 +2,8 @@ import { Box, InputBox } from '@rocket.chat/fuselage'; import React, { FC, memo } from 'react'; const ComposerSkeleton: FC = () => ( - <Box pi='x24' pb='x16' display='flex'> - <InputBox.Skeleton /> + <Box padding={24} display='flex'> + <InputBox.Skeleton height={52} /> </Box> ); export default memo(ComposerSkeleton); diff --git a/apps/meteor/client/views/room/Room/LazyComponent.js b/apps/meteor/client/views/room/Room/LazyComponent.js deleted file mode 100644 index f79adf792d5..00000000000 --- a/apps/meteor/client/views/room/Room/LazyComponent.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Suspense } from 'react'; - -import VerticalBar from '../../../components/VerticalBar'; - -const LazyComponent = ({ template: TabbarTemplate, ...props }) => ( - <Suspense fallback={<VerticalBar.Skeleton />}> - <TabbarTemplate {...props} /> - </Suspense> -); - -export default LazyComponent; diff --git a/apps/meteor/client/views/room/Room/Room.tsx b/apps/meteor/client/views/room/Room/Room.tsx index 82bfaa845d1..55435c30257 100644 --- a/apps/meteor/client/views/room/Room/Room.tsx +++ b/apps/meteor/client/views/room/Room/Room.tsx @@ -1,75 +1,66 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, ReactElement } from 'react'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { createElement, memo, ReactElement, Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; -import Announcement from '../Announcement'; +import VerticalBarSkeleton from '../../../components/VerticalBar/VerticalBarSkeleton'; import Header from '../Header'; -import BlazeTemplate from '../components/BlazeTemplate'; -import { RoomTemplate } from '../components/RoomTemplate/RoomTemplate'; import VerticalBarOldActions from '../components/VerticalBarOldActions'; +import RoomBody from '../components/body/RoomBody'; import { useRoom } from '../contexts/RoomContext'; import AppsContextualBar from '../contextualBar/Apps'; import { useAppsContextualBar } from '../hooks/useAppsContextualBar'; +import RoomLayout from '../layout/RoomLayout'; import { SelectedMessagesProvider } from '../providers/SelectedMessagesProvider'; -import { useTab, useTabBarOpen, useTabBarClose, useTabBarOpenUserInfo } from '../providers/ToolboxProvider'; -import LazyComponent from './LazyComponent'; +import { useTab, useTabBarAPI } from '../providers/ToolboxProvider'; -export const Room = (): ReactElement => { - const room = useRoom(); +const Room = (): ReactElement => { const t = useTranslation(); - const tab = useTab(); - const open = useTabBarOpen(); - const close = useTabBarClose(); - const openUserInfo = useTabBarOpenUserInfo(); - const isLayoutEmbedded = useEmbeddedLayout(); - const hideFlexTab = useUserPreference('hideFlexTab'); - const isOpen = useMutableCallback(() => !!tab?.template); + const room = useRoom(); + + const tabBar = useTabBarAPI(); + const tab = useTab(); const appsContextualBarContext = useAppsContextualBar(); - const tabBar = useMemo(() => ({ open, close, isOpen, openUserInfo }), [open, close, isOpen, openUserInfo]); - return ( - <RoomTemplate aria-label={t('Channel')} data-qa-rc-room={room._id}> - <RoomTemplate.Header> - <Header room={room} /> - </RoomTemplate.Header> - <RoomTemplate.Body> - {!isLayoutEmbedded && room.announcement && <Announcement announcement={room.announcement} announcementDetails={undefined} />} - <BlazeTemplate onClick={hideFlexTab ? close : undefined} name='roomOld' tabBar={tabBar} rid={room._id} _id={room._id} /> - </RoomTemplate.Body> - {tab && ( - <RoomTemplate.Aside data-qa-tabbar-name={tab.id}> + <RoomLayout + aria-label={t('Channel')} + data-qa-rc-room={room._id} + header={<Header room={room} />} + body={<RoomBody />} + aside={ + (tab && ( <ErrorBoundary fallback={null}> <SelectedMessagesProvider> {typeof tab.template === 'string' && ( <VerticalBarOldActions {...tab} name={tab.template} tabBar={tabBar} rid={room._id} _id={room._id} /> )} - {typeof tab.template !== 'string' && ( - <LazyComponent template={tab.template} tabBar={tabBar} rid={room._id} teamId={room.teamId} _id={room._id} /> + {typeof tab.template !== 'string' && typeof tab.template !== 'undefined' && ( + <Suspense fallback={<VerticalBarSkeleton />}> + {createElement(tab.template, { tabBar, _id: room._id, rid: room._id, teamId: room.teamId })} + </Suspense> )} </SelectedMessagesProvider> </ErrorBoundary> - </RoomTemplate.Aside> - )} - {appsContextualBarContext && ( - <RoomTemplate.Aside data-qa-tabbar-name={appsContextualBarContext.viewId}> - <SelectedMessagesProvider> - <ErrorBoundary fallback={null}> - <LazyComponent - template={AppsContextualBar} - viewId={appsContextualBarContext.viewId} - roomId={appsContextualBarContext.roomId} - payload={appsContextualBarContext.payload} - appId={appsContextualBarContext.appId} - /> - </ErrorBoundary> - </SelectedMessagesProvider> - </RoomTemplate.Aside> - )} - </RoomTemplate> + )) || + (appsContextualBarContext && ( + <ErrorBoundary fallback={null}> + <SelectedMessagesProvider> + <Suspense fallback={<VerticalBarSkeleton />}> + <AppsContextualBar + viewId={appsContextualBarContext.viewId} + roomId={appsContextualBarContext.roomId} + payload={appsContextualBarContext.payload} + appId={appsContextualBarContext.appId} + /> + </Suspense> + </SelectedMessagesProvider> + </ErrorBoundary> + )) + } + /> ); }; + +export default memo(Room); diff --git a/apps/meteor/client/views/room/Room/RoomNotFound/index.ts b/apps/meteor/client/views/room/Room/RoomNotFound/index.ts deleted file mode 100644 index 3ea73749faa..00000000000 --- a/apps/meteor/client/views/room/Room/RoomNotFound/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './RoomNotFound'; diff --git a/apps/meteor/client/views/room/Room/RoomSkeleton.tsx b/apps/meteor/client/views/room/Room/RoomSkeleton.tsx deleted file mode 100644 index 523477d13a1..00000000000 --- a/apps/meteor/client/views/room/Room/RoomSkeleton.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Skeleton, Box } from '@rocket.chat/fuselage'; -import React, { FC, memo } from 'react'; - -import Header from '../../../components/Header'; -import VerticalBarSkeleton from '../../../components/VerticalBar/VerticalBarSkeleton'; -import { RoomTemplate } from '../components/RoomTemplate/RoomTemplate'; -import ComposerSkeleton from './ComposerSkeleton'; - -const RoomSkeleton: FC = () => ( - <RoomTemplate> - <RoomTemplate.Header> - <Header> - <Header.Avatar> - <Skeleton variant='rect' width={36} height={36} /> - </Header.Avatar> - <Header.Content> - <Header.Content.Row> - <Skeleton width='10%' /> - </Header.Content.Row> - <Header.Content.Row> - <Skeleton width='30%' /> - </Header.Content.Row> - </Header.Content> - </Header> - </RoomTemplate.Header> - <RoomTemplate.Body> - <Box display='flex' height='100%' justifyContent='flex-start' flexDirection='column'> - <Box pi='x24' pb='x16' display='flex'> - <Box> - <Skeleton variant='rect' width={36} height={36} /> - </Box> - <Box mis='x8' flexGrow={1}> - <Skeleton width='100%' /> - <Skeleton width='69%' /> - </Box> - </Box> - <Box pi='x24' pb='x16' display='flex'> - <Box> - <Skeleton variant='rect' width={36} height={36} /> - </Box> - <Box mis='x8' flexGrow={1}> - <Skeleton width='100%' /> - <Skeleton width='40%' /> - </Box> - </Box> - </Box> - <ComposerSkeleton /> - </RoomTemplate.Body> - <RoomTemplate.Aside> - <VerticalBarSkeleton /> - </RoomTemplate.Aside> - </RoomTemplate> -); -export default memo(RoomSkeleton); diff --git a/apps/meteor/client/views/room/Room/RoomWithData.tsx b/apps/meteor/client/views/room/Room/RoomWithData.tsx deleted file mode 100644 index 0da9132bd4a..00000000000 --- a/apps/meteor/client/views/room/Room/RoomWithData.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { ReactElement } from 'react'; - -import { useOpenedRoom } from '../../../lib/RoomManager'; -import RoomProvider from '../providers/RoomProvider'; -import { Room } from './Room'; - -const RoomWithData = (): ReactElement | null => { - const rid = useOpenedRoom(); - return rid ? ( - <RoomProvider rid={rid}> - <Room /> - </RoomProvider> - ) : null; -}; - -export default RoomWithData; diff --git a/apps/meteor/client/views/room/Room/index.ts b/apps/meteor/client/views/room/Room/index.ts deleted file mode 100644 index 8518d6a8fd7..00000000000 --- a/apps/meteor/client/views/room/Room/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './RoomWithData'; diff --git a/apps/meteor/client/views/room/Room/RoomNotFound/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx similarity index 75% rename from apps/meteor/client/views/room/Room/RoomNotFound/RoomNotFound.tsx rename to apps/meteor/client/views/room/RoomNotFound.tsx index 2e6f301e24f..b4315b76225 100644 --- a/apps/meteor/client/views/room/Room/RoomNotFound/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -2,9 +2,9 @@ import { States, StatesIcon, StatesTitle, StatesSubtitle, Box, StatesActions, St import { useLayout, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; -import BurgerMenu from '../../../../components/BurgerMenu'; -import TemplateHeader from '../../../../components/Header'; -import { RoomTemplate } from '../../components/RoomTemplate/RoomTemplate'; +import BurgerMenu from '../../components/BurgerMenu'; +import TemplateHeader from '../../components/Header'; +import RoomLayout from './layout/RoomLayout'; const RoomNotFound = (): ReactElement => { const { isMobile } = useLayout(); @@ -16,17 +16,17 @@ const RoomNotFound = (): ReactElement => { }; return ( - <RoomTemplate> - <RoomTemplate.Header> - {isMobile && ( + <RoomLayout + header={ + isMobile && ( <TemplateHeader justifyContent='start'> <TemplateHeader.ToolBox> <BurgerMenu /> </TemplateHeader.ToolBox> </TemplateHeader> - )} - </RoomTemplate.Header> - <RoomTemplate.Body> + ) + } + body={ <Box display='flex' justifyContent='center' height='full'> <States> <StatesIcon name='magnifier' /> @@ -37,8 +37,8 @@ const RoomNotFound = (): ReactElement => { </StatesActions> </States> </Box> - </RoomTemplate.Body> - </RoomTemplate> + } + /> ); }; diff --git a/apps/meteor/client/views/room/RoomSkeleton.tsx b/apps/meteor/client/views/room/RoomSkeleton.tsx new file mode 100644 index 00000000000..30bd33db21e --- /dev/null +++ b/apps/meteor/client/views/room/RoomSkeleton.tsx @@ -0,0 +1,54 @@ +import { Skeleton, Box } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +import Header from '../../components/Header'; +import VerticalBarSkeleton from '../../components/VerticalBar/VerticalBarSkeleton'; +import ComposerSkeleton from './Room/ComposerSkeleton'; +import RoomLayout from './layout/RoomLayout'; + +const RoomSkeleton = (): ReactElement => ( + <RoomLayout + header={ + <Header> + <Header.Avatar> + <Skeleton variant='rect' width={36} height={36} /> + </Header.Avatar> + <Header.Content> + <Header.Content.Row> + <Skeleton width='10%' /> + </Header.Content.Row> + <Header.Content.Row> + <Skeleton width='30%' /> + </Header.Content.Row> + </Header.Content> + </Header> + } + body={ + <> + <Box display='flex' height='100%' justifyContent='flex-start' flexDirection='column'> + <Box pi='x24' pb='x16' display='flex'> + <Box> + <Skeleton variant='rect' width={36} height={36} /> + </Box> + <Box mis='x8' flexGrow={1}> + <Skeleton width='100%' /> + <Skeleton width='69%' /> + </Box> + </Box> + <Box pi='x24' pb='x16' display='flex'> + <Box> + <Skeleton variant='rect' width={36} height={36} /> + </Box> + <Box mis='x8' flexGrow={1}> + <Skeleton width='100%' /> + <Skeleton width='40%' /> + </Box> + </Box> + </Box> + <ComposerSkeleton /> + </> + } + aside={<VerticalBarSkeleton />} + /> +); +export default RoomSkeleton; diff --git a/apps/meteor/client/views/room/components/RoomTemplate/RoomTemplate.tsx b/apps/meteor/client/views/room/components/RoomTemplate/RoomTemplate.tsx deleted file mode 100644 index 682a78c55d6..00000000000 --- a/apps/meteor/client/views/room/components/RoomTemplate/RoomTemplate.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; -import flattenChildren from 'react-keyed-flatten-children'; - -import VerticalBar from '../../../../components/VerticalBar/VerticalBar'; -import { Aside } from './slots/Aside'; -import { Body } from './slots/Body'; -import { Footer } from './slots/Footer'; -import { Header } from './slots/Header'; - -export const RoomTemplate: FC & { - Header: FC; - Body: FC; - Footer: FC; - Aside: FC; -} = ({ children, ...props }) => { - const c = flattenChildren(children); - const header = c.filter((child) => (child as any).type === RoomTemplate.Header); - const body = c.filter((child) => (child as any).type === RoomTemplate.Body); - const footer = c.filter((child) => (child as any).type === RoomTemplate.Footer); - const aside = c.filter((child) => (child as any).type === RoomTemplate.Aside); - - return ( - <Box is='main' h='full' display='flex' flexDirection='column' {...props}> - {header.length > 0 && header} - <Box display='flex' flexGrow={1} overflow='hidden' height='full' position='relative'> - <Box display='flex' flexDirection='column' flexGrow={1}> - <Box is='div' display='flex' flexDirection='column' flexGrow={1}> - {body} - </Box> - {footer.length > 0 && <Box is='footer'>{footer}</Box>} - </Box> - {aside.length > 0 && <VerticalBar is='aside'>{aside}</VerticalBar>} - </Box> - </Box> - ); -}; - -RoomTemplate.Header = Header; -RoomTemplate.Body = Body; -RoomTemplate.Footer = Footer; -RoomTemplate.Aside = Aside; - -export default RoomTemplate; diff --git a/apps/meteor/client/views/room/components/RoomTemplate/slots/Aside.tsx b/apps/meteor/client/views/room/components/RoomTemplate/slots/Aside.tsx deleted file mode 100644 index 386fe26382b..00000000000 --- a/apps/meteor/client/views/room/components/RoomTemplate/slots/Aside.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { FC } from 'react'; - -export const Aside: FC = function Aside({ children }) { - return <>{children}</>; -}; diff --git a/apps/meteor/client/views/room/components/RoomTemplate/slots/Body.tsx b/apps/meteor/client/views/room/components/RoomTemplate/slots/Body.tsx deleted file mode 100644 index 7c08bb05d3e..00000000000 --- a/apps/meteor/client/views/room/components/RoomTemplate/slots/Body.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { FC } from 'react'; - -export const Body: FC = function Body({ children }) { - return <>{children}</>; -}; diff --git a/apps/meteor/client/views/room/components/RoomTemplate/slots/Footer.tsx b/apps/meteor/client/views/room/components/RoomTemplate/slots/Footer.tsx deleted file mode 100644 index a5ee444acaf..00000000000 --- a/apps/meteor/client/views/room/components/RoomTemplate/slots/Footer.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { FC } from 'react'; - -export const Footer: FC = function Footer({ children }) { - return <>{children}</>; -}; diff --git a/apps/meteor/client/views/room/components/RoomTemplate/slots/Header.tsx b/apps/meteor/client/views/room/components/RoomTemplate/slots/Header.tsx deleted file mode 100644 index f2082df2ac8..00000000000 --- a/apps/meteor/client/views/room/components/RoomTemplate/slots/Header.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { FC } from 'react'; - -export const Header: FC = function Header({ children }) { - return <>{children}</>; -}; diff --git a/apps/meteor/client/views/room/components/body/ComposerContainer.tsx b/apps/meteor/client/views/room/components/body/ComposerContainer.tsx new file mode 100644 index 00000000000..2b7cd16de7a --- /dev/null +++ b/apps/meteor/client/views/room/components/body/ComposerContainer.tsx @@ -0,0 +1,104 @@ +import { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { Blaze } from 'meteor/blaze'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Template } from 'meteor/templating'; +import React, { memo, ReactElement, useCallback, useEffect, useRef } from 'react'; + +import { ChatMessages } from '../../../../../app/ui'; +import { RoomManager } from '../../../../../app/ui-utils/client'; +import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import ComposerSkeleton from '../../Room/ComposerSkeleton'; + +type ComposerContainerProps = { + rid: IRoom['_id']; + subscription?: ISubscription; + chatMessagesInstance: ChatMessages; + onResize?: () => void; +}; + +const ComposerContainer = ({ rid, subscription, chatMessagesInstance, onResize }: ComposerContainerProps): ReactElement => { + const isLayoutEmbedded = useEmbeddedLayout(); + const showFormattingTips = useSetting('Message_ShowFormattingTips') as boolean; + + const messageBoxViewRef = useRef<Blaze.View>(); + const messageBoxViewDataRef = useRef( + new ReactiveVar({ + rid, + subscription, + isEmbedded: isLayoutEmbedded, + showFormattingTips: showFormattingTips && !isLayoutEmbedded, + onResize, + }), + ); + + useEffect(() => { + messageBoxViewDataRef.current.set({ + rid, + subscription, + isEmbedded: isLayoutEmbedded, + showFormattingTips: showFormattingTips && !isLayoutEmbedded, + onResize, + }); + }, [isLayoutEmbedded, onResize, rid, showFormattingTips, subscription]); + + const footerRef = useCallback( + (footer: HTMLElement | null) => { + if (footer) { + messageBoxViewRef.current = Blaze.renderWithData( + Template.messageBox, + () => ({ + ...messageBoxViewDataRef.current.get(), + onInputChanged: (input: HTMLTextAreaElement): void => { + chatMessagesInstance.initializeInput(input, { rid }); + }, + onKeyUp: ( + event: KeyboardEvent, + { + rid, + tmid, + }: { + rid: string; + tmid?: string | undefined; + }, + ) => chatMessagesInstance.keyup(event, { rid, tmid }), + onKeyDown: (event: KeyboardEvent) => chatMessagesInstance.keydown(event), + onSend: ( + event: Event, + params: { + rid: string; + tmid?: string; + value: string; + tshow?: boolean; + }, + done?: () => void, + ) => chatMessagesInstance.send(event, params, done), + }), + footer, + ); + return; + } + + if (messageBoxViewRef.current) { + Blaze.remove(messageBoxViewRef.current); + messageBoxViewRef.current = undefined; + } + }, + [rid, chatMessagesInstance], + ); + + const subscriptionReady = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(rid)?.streamActive ?? false, [rid])); + + if (!subscriptionReady) { + return ( + <footer className='footer'> + <ComposerSkeleton /> + </footer> + ); + } + + return <footer ref={footerRef} className='footer' />; +}; + +export default memo(ComposerContainer); diff --git a/apps/meteor/client/views/room/components/body/DropTargetOverlay.tsx b/apps/meteor/client/views/room/components/body/DropTargetOverlay.tsx new file mode 100644 index 00000000000..c28f7c2b324 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/DropTargetOverlay.tsx @@ -0,0 +1,95 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { DragEvent, memo, ReactElement, ReactNode } from 'react'; + +import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; + +type DropTargetOverlayProps = { + enabled: boolean; + reason?: ReactNode; + onFileDrop?: (files: File[]) => void; + visible?: boolean; + onDismiss?: () => void; +}; + +function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null { + const t = useTranslation(); + + const handleDragLeave = useMutableCallback((event: DragEvent) => { + event.stopPropagation(); + onDismiss?.(); + }); + + const handleDragOver = useMutableCallback((event: DragEvent) => { + event.stopPropagation(); + + event.preventDefault(); + event.dataTransfer.dropEffect = ['move', 'linkMove'].includes(event.dataTransfer.effectAllowed) ? 'move' : 'copy'; + }); + + const formatDateAndTime = useFormatDateAndTime(); + + const handleDrop = useMutableCallback(async (event: DragEvent) => { + event.stopPropagation(); + onDismiss?.(); + + event.preventDefault(); + + const files = Array.from(event.dataTransfer.files); + + if (event.dataTransfer.types.includes('text/uri-list') && event.dataTransfer.types.includes('text/html')) { + const fragment = document.createRange().createContextualFragment(event.dataTransfer.getData('text/html')); + for await (const { src } of Array.from(fragment.querySelectorAll('img'))) { + try { + const response = await fetch(src); + const data = await response.blob(); + const extension = (await import('../../../../../app/utils/lib/mimeTypes')).mime.extension(data.type); + const filename = `File - ${formatDateAndTime(new Date())}.${extension}`; + const file = new File([data], filename, { type: data.type }); + files.push(file); + } catch (error) { + console.warn(error); + } + } + } + + onFileDrop?.(files); + }); + + if (!visible) { + return null; + } + + return ( + <Box + role='dialog' + data-qa='DropTargetOverlay' + position='absolute' + zIndex={1_000_000} + inset={0} + display='flex' + alignItems='center' + justifyContent='center' + fontScale='hero' + textAlign='center' + backgroundColor='rgba(255, 255, 255, 0.8)' + borderWidth={4} + borderStyle='dashed' + borderColor='currentColor' + color={enabled ? 'primary' : 'danger'} + className={css` + animation-name: zoomIn; + animation-duration: 0.1s; + `} + onDragLeave={handleDragLeave} + onDragOver={handleDragOver} + onDrop={handleDrop} + > + {enabled ? t('Drop_to_upload_file') : reason} + </Box> + ); +} + +export default memo(DropTargetOverlay); diff --git a/apps/meteor/client/views/room/components/body/ErroredUploadProgressIndicator.tsx b/apps/meteor/client/views/room/components/body/ErroredUploadProgressIndicator.tsx new file mode 100644 index 00000000000..86ccbf7f22d --- /dev/null +++ b/apps/meteor/client/views/room/components/body/ErroredUploadProgressIndicator.tsx @@ -0,0 +1,29 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useCallback } from 'react'; + +import { Uploading } from '../../../../../app/ui/client/lib/fileUpload'; + +type ErroredUploadProgressIndicatorProps = { + id: Uploading['id']; + error: string; + onClose?: (id: Uploading['id']) => void; +}; + +const ErroredUploadProgressIndicator = ({ id, error, onClose }: ErroredUploadProgressIndicatorProps): ReactElement => { + const t = useTranslation(); + + const handleCloseClick = useCallback(() => { + onClose?.(id); + }, [id, onClose]); + + return ( + <div className='upload-progress color-primary-action-color background-component-color error-background error-border'> + <div className='upload-progress-text'>{error}</div> + <button type='button' className='upload-progress-close' onClick={handleCloseClick}> + {t('close')} + </button> + </div> + ); +}; + +export default ErroredUploadProgressIndicator; diff --git a/apps/meteor/client/views/room/components/body/JumpToRecentMessagesBar.tsx b/apps/meteor/client/views/room/components/body/JumpToRecentMessagesBar.tsx new file mode 100644 index 00000000000..e334ee8892e --- /dev/null +++ b/apps/meteor/client/views/room/components/body/JumpToRecentMessagesBar.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, UIEvent } from 'react'; + +import { isTruthy } from '../../../../../lib/isTruthy'; + +type JumpToRecentMessagesBarProps = { + visible: boolean; + onClick: (event: UIEvent) => void; +}; + +const JumpToRecentMessagesBar = ({ visible, onClick }: JumpToRecentMessagesBarProps): ReactElement => { + const t = useTranslation(); + + return ( + <div className={[`jump-recent`, `background-component-color`, !visible && 'not'].filter(isTruthy).join(' ')}> + <button type='button' onClick={onClick}> + {t('Jump_to_recent_messages')} <i className='icon-level-down' /> + </button> + </div> + ); +}; + +export default JumpToRecentMessagesBar; diff --git a/apps/meteor/client/views/room/components/body/LeaderBar.tsx b/apps/meteor/client/views/room/components/body/LeaderBar.tsx new file mode 100644 index 00000000000..edc46133096 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/LeaderBar.tsx @@ -0,0 +1,62 @@ +import { IUser } from '@rocket.chat/core-typings'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement, ReactNode, UIEvent, useCallback, useMemo } from 'react'; + +import { isTruthy } from '../../../../../lib/isTruthy'; +import UserAvatar from '../../../../components/avatar/UserAvatar'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; + +type LeaderBarProps = { + name: IUser['name']; + username: IUser['username']; + status?: 'online' | 'offline' | 'busy' | 'away'; + statusText?: ReactNode; + visible: boolean; + onAvatarClick?: (event: UIEvent, username: IUser['username']) => void; +}; + +const LeaderBar = ({ name, username, status = 'offline', statusText, visible, onAvatarClick }: LeaderBarProps): ReactElement => { + const t = useTranslation(); + + const chatNowLink = useMemo(() => roomCoordinator.getRouteLink('d', { name: username }) || undefined, [username]); + + const handleAvatarClick = useCallback( + (event: UIEvent) => { + onAvatarClick?.(event, username); + }, + [onAvatarClick, username], + ); + + if (!username) { + throw new Error('username is required'); + } + + return ( + <div + className={[ + `room-leader`, + `message`, + `color-primary-font-color`, + `content-background-color`, + `border-component-color`, + !visible && 'animated-hidden', + ] + .filter(isTruthy) + .join(' ')} + > + <button type='button' className='thumb user-card-message' onClick={handleAvatarClick}> + <UserAvatar size='x40' username={username} /> + </button> + <div className='leader-name'>{name}</div> + <div className='leader-status userStatus'> + <span className={`color-ball status-bg-${status}`} /> + <span className='status-text leader-status-text'>{statusText ?? t(status)}</span> + </div> + <a className='chat-now' href={chatNowLink}> + {t('Chat_Now')} + </a> + </div> + ); +}; + +export default memo(LeaderBar); diff --git a/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx b/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx new file mode 100644 index 00000000000..8647c196fb5 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx @@ -0,0 +1,90 @@ +import { IMessage, IRoom, MessageTypesValues } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { Blaze } from 'meteor/blaze'; +import { Template } from 'meteor/templating'; +import React, { memo, ReactElement, useCallback, useRef } from 'react'; + +import { ChatMessage } from '../../../../../app/models/client'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { useMessageContext } from './useMessageContext'; + +type LegacyMessageTemplateListProps = { + room: IRoom; +}; + +const LegacyMessageTemplateList = ({ room }: LegacyMessageTemplateListProps): ReactElement => { + const messageContext = useMessageContext(room); + + const hideSystemMessages = useSetting('Hide_System_Messages') as MessageTypesValues[]; + + const messagesHistory = useReactiveValue( + useCallback(() => { + const settingValues = Array.isArray(room?.sysMes) ? (room.sysMes as MessageTypesValues[]) : hideSystemMessages || []; + const hideMessagesOfType = new Set( + settingValues.reduce( + (array: MessageTypesValues[], value: MessageTypesValues) => [ + ...array, + ...(value === 'mute_unmute' ? (['user-muted', 'user-unmuted'] as const) : ([value] as const)), + ], + [], + ), + ); + const query: Mongo.Query<IMessage> = { + rid: room._id, + _hidden: { $ne: true }, + $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + ...(hideMessagesOfType.size && { t: { $nin: Array.from(hideMessagesOfType.values()) } }), + }; + + const options = { + sort: { + ts: 1, + }, + }; + + return ChatMessage.find(query, options).fetch(); + }, [hideSystemMessages, room._id, room.sysMes]), + ); + + const viewsRef = useRef<Map<string, Blaze.View>>(new Map()); + + const messageRef = useCallback( + (message: IMessage, index: number) => (node: HTMLLIElement | null) => { + if (node?.parentElement) { + const view = Blaze.renderWithData( + Template.message, + () => ({ + showRoles: true, + index, + shouldCollapseReplies: false, + msg: message, + ...messageContext, + }), + node.parentElement, + node, + ); + + viewsRef.current.set(message._id, view); + } + + if (!node && viewsRef.current.has(message._id)) { + const view = viewsRef.current.get(message._id); + if (view) { + Blaze.remove(view); + } + viewsRef.current.delete(message._id); + } + }, + [messageContext], + ); + + return ( + <> + {messagesHistory.map((message, index) => ( + <li key={message._id} ref={messageRef(message, index)} /> + ))} + </> + ); +}; + +export default memo(LegacyMessageTemplateList); diff --git a/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx b/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx new file mode 100644 index 00000000000..b3277998f24 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx @@ -0,0 +1,11 @@ +import React, { ReactElement } from 'react'; + +const LoadingMessagesIndicator = (): ReactElement => ( + <div className='loading-animation'> + <div className='bounce bounce1' /> + <div className='bounce bounce2' /> + <div className='bounce bounce3' /> + </div> +); + +export default LoadingMessagesIndicator; diff --git a/apps/meteor/client/views/room/components/body/NewMessagesButton.tsx b/apps/meteor/client/views/room/components/body/NewMessagesButton.tsx new file mode 100644 index 00000000000..3046b3f8bee --- /dev/null +++ b/apps/meteor/client/views/room/components/body/NewMessagesButton.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, UIEvent } from 'react'; + +import { isTruthy } from '../../../../../lib/isTruthy'; + +type NewMessagesButtonProps = { + visible: boolean; + onClick: (event: UIEvent) => void; +}; + +const NewMessagesButton = ({ visible, onClick }: NewMessagesButtonProps): ReactElement => { + const t = useTranslation(); + + return ( + <button + type='button' + className={[`new-message`, `background-primary-action-color`, `color-content-background-color`, !visible && 'not'] + .filter(isTruthy) + .join(' ')} + onClick={onClick} + > + <i className='icon-down-big' /> + {t('New_messages')} + </button> + ); +}; + +export default NewMessagesButton; diff --git a/apps/meteor/client/views/room/components/body/RetentionPolicyWarning.tsx b/apps/meteor/client/views/room/components/body/RetentionPolicyWarning.tsx new file mode 100644 index 00000000000..7defd9a2020 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/RetentionPolicyWarning.tsx @@ -0,0 +1,38 @@ +import { Icon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useMemo } from 'react'; + +import { useFormatRelativeTime } from '../../../../hooks/useFormatRelativeTime'; + +type RetentionPolicyWarningProps = { + filesOnly: boolean; + excludePinned: boolean; + maxAge: number; +}; + +const RetentionPolicyWarning = ({ filesOnly, excludePinned, maxAge }: RetentionPolicyWarningProps): ReactElement => { + const t = useTranslation(); + + const formatRelativeTime = useFormatRelativeTime(); + const time = useMemo(() => formatRelativeTime(maxAge), [formatRelativeTime, maxAge]); + + if (filesOnly) { + return ( + <div className='start__purge-warning error-background error-border error-color'> + <Icon name='warning' size='x20' />{' '} + {excludePinned + ? t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time }) + : t('RetentionPolicy_RoomWarning_FilesOnly', { time })} + </div> + ); + } + + return ( + <div className='start__purge-warning error-background error-border error-color'> + <Icon name='warning' size='x20' />{' '} + {excludePinned ? t('RetentionPolicy_RoomWarning_Unpinned', { time }) : t('RetentionPolicy_RoomWarning', { time })} + </div> + ); +}; + +export default RetentionPolicyWarning; diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx new file mode 100644 index 00000000000..4050ff1ffc4 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -0,0 +1,631 @@ +import { IMessage, isEditedMessage, isOmnichannelRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import { + useCurrentRoute, + usePermission, + useQueryStringParameter, + useRole, + useSession, + useSetting, + useTranslation, + useUser, + useUserPreference, +} from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { Subscriptions, ChatMessage, RoomRoles, Users } from '../../../../../app/models/client'; +import { readMessage, RoomHistoryManager } from '../../../../../app/ui-utils/client'; +import { openUserCard } from '../../../../../app/ui/client/lib/UserCard'; +import { Uploading } from '../../../../../app/ui/client/lib/fileUpload'; +import { CommonRoomTemplateInstance } from '../../../../../app/ui/client/views/app/lib/CommonRoomTemplateInstance'; +import { getCommonRoomEvents } from '../../../../../app/ui/client/views/app/lib/getCommonRoomEvents'; +import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; +import { callbacks } from '../../../../../lib/callbacks'; +import { isTruthy } from '../../../../../lib/isTruthy'; +import { withDebouncing, withThrottling } from '../../../../../lib/utils/highOrderFunctions'; +import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { RoomManager as NewRoomManager } from '../../../../lib/RoomManager'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import Announcement from '../../Announcement'; +import { MessageList } from '../../MessageList/MessageList'; +import { useRoom } from '../../contexts/RoomContext'; +import { useTabBarAPI } from '../../providers/ToolboxProvider'; +import ComposerContainer from './ComposerContainer'; +import DropTargetOverlay from './DropTargetOverlay'; +import JumpToRecentMessagesBar from './JumpToRecentMessagesBar'; +import LeaderBar from './LeaderBar'; +import LegacyMessageTemplateList from './LegacyMessageTemplateList'; +import LoadingMessagesIndicator from './LoadingMessagesIndicator'; +import NewMessagesButton from './NewMessagesButton'; +import RetentionPolicyWarning from './RetentionPolicyWarning'; +import RoomForeword from './RoomForeword'; +import UnreadMessagesIndicator from './UnreadMessagesIndicator'; +import UploadProgressIndicator from './UploadProgressIndicator'; +import { useChatMessages } from './useChatMessages'; +import { useFileUploadDropTarget } from './useFileUploadDropTarget'; +import { useRetentionPolicy } from './useRetentionPolicy'; +import { useRoomRoles } from './useRoomRoles'; +import { useUnreadMessages } from './useUnreadMessages'; + +const RoomBody = (): ReactElement => { + const t = useTranslation(); + const isLayoutEmbedded = useEmbeddedLayout(); + const room = useRoom(); + const user = useUser(); + const tabBar = useTabBarAPI(); + const admin = useRole('admin'); + const subscription = useReactiveValue( + useCallback(() => Subscriptions.findOne({ rid: room._id }) as ISubscription | undefined, [room._id]), + ); + + const [lastMessage, setLastMessage] = useState<Date | undefined>(); + const [hideLeaderHeader, setHideLeaderHeader] = useState(false); + const [hasNewMessages, setHasNewMessages] = useState(false); + + const hideFlexTab = useUserPreference<boolean>('hideFlexTab'); + const hideUsernames = useUserPreference<boolean>('hideUsernames'); + const displayAvatars = useUserPreference<boolean>('displayAvatars'); + const useLegacyMessageTemplate = useUserPreference<boolean>('useLegacyMessageTemplate'); + const viewMode = useUserPreference<number>('messageViewMode'); + + const wrapperRef = useRef<HTMLDivElement | null>(null); + const messagesBoxRef = useRef<HTMLDivElement | null>(null); + const atBottomRef = useRef(!useQueryStringParameter('msg')); + const lastScrollTopRef = useRef(0); + + const chatMessagesInstance = useChatMessages(room._id, wrapperRef); + useRoomRoles(room._id); + const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(room); + + const _isAtBottom = useCallback((scrollThreshold = 0) => { + const wrapper = wrapperRef.current; + + if (!wrapper) { + return false; + } + + if (isAtBottom(wrapper, scrollThreshold)) { + setHasNewMessages(false); + return true; + } + return false; + }, []); + + const sendToBottom = useCallback(() => { + const wrapper = wrapperRef.current; + + wrapper?.scrollTo(30, wrapper.scrollHeight); + setHasNewMessages(false); + }, []); + + const sendToBottomIfNecessary = useCallback(() => { + if (atBottomRef.current === true) { + sendToBottom(); + } + }, [sendToBottom]); + + const checkIfScrollIsAtBottom = useCallback(() => { + atBottomRef.current = _isAtBottom(100); + }, [_isAtBottom]); + + const handleNewMessageButtonClick = useCallback(() => { + atBottomRef.current = true; + sendToBottomIfNecessary(); + chatMessagesInstance.input?.focus(); + }, [chatMessagesInstance, sendToBottomIfNecessary]); + + const handleJumpToRecentButtonClick = useCallback(() => { + atBottomRef.current = true; + RoomHistoryManager.clear(room._id); + RoomHistoryManager.getMoreIfIsEmpty(room._id); + }, [room._id]); + + const [unread, setUnreadCount] = useUnreadMessages(room); + + const uploading = useSession('uploading') as Uploading[]; + + const messageViewMode = useMemo(() => { + const modes = ['', 'cozy', 'compact'] as const; + return modes[viewMode ?? 0] ?? modes[0]; + }, [viewMode]); + + const hasMore = useReactiveValue(useCallback(() => RoomHistoryManager.hasMore(room._id), [room._id])); + + const hasMoreNext = useReactiveValue(useCallback(() => RoomHistoryManager.hasMoreNext(room._id), [room._id])); + + const isLoading = useReactiveValue(useCallback(() => RoomHistoryManager.isLoading(room._id), [room._id])); + + const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead') as boolean | undefined; + + const canPreviewChannelRoom = usePermission('preview-c-room'); + + const subscribed = !!subscription; + + const canPreview = useMemo(() => { + if (room && room.t !== 'c') { + return true; + } + + if (allowAnonymousRead === true) { + return true; + } + + if (canPreviewChannelRoom) { + return true; + } + + return subscribed; + }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); + + const roomRoles = useReactiveValue<ISubscription | undefined>( + useCallback( + () => + RoomRoles.findOne({ + 'rid': room._id, + 'roles': 'leader', + 'u._id': { $ne: user?._id }, + }), + [room._id, user?._id], + ), + ); + + const useRealName = useSetting('UI_Use_Real_Name') as boolean; + + const roomLeader = useReactiveValue( + useCallback(() => { + if (!roomRoles) { + return; + } + + const leaderUser = Users.findOne({ _id: roomRoles.u._id }, { fields: { status: 1, statusText: 1 } }) as IUser | undefined; + + return { + ...roomRoles.u, + name: useRealName ? roomRoles.u.name || roomRoles.u.username : roomRoles.u.username, + status: leaderUser?.status, + statusText: leaderUser?.statusText, + } as const; + }, [roomRoles, useRealName]), + ); + + const handleOpenUserCardButtonClick = useCallback( + (event: UIEvent, username: IUser['username']) => { + openUserCard({ + username, + rid: room._id, + target: event.currentTarget, + open: (event: MouseEvent) => { + event.preventDefault(); + if (username) tabBar.openUserInfo(username); + }, + }); + }, + [room._id, tabBar], + ); + + const handleUnreadBarJumpToButtonClick = useCallback(() => { + const rid = room._id; + const { firstUnread } = RoomHistoryManager.getRoom(rid) ?? {}; + let message = firstUnread?.get(); + if (!message) { + message = ChatMessage.findOne({ rid, ts: { $gt: subscription?.ls } }, { sort: { ts: 1 }, limit: 1 }); + } + RoomHistoryManager.getSurroundingMessages(message, atBottomRef); + }, [room._id, subscription?.ls]); + + const handleMarkAsReadButtonClick = useCallback(() => { + readMessage.readNow(room._id); + }, [room._id]); + + const handleUploadProgressClose = useCallback((id: Uploading['id']) => { + Session.set(`uploading-cancel-${id}`, true); + }, []); + + const retentionPolicy = useRetentionPolicy(room); + + useEffect(() => { + callbacks.add( + 'streamNewMessage', + (msg: IMessage) => { + if (room._id !== msg.rid || (isEditedMessage(msg) && msg.editedAt) || msg.tmid) { + return; + } + + if (msg.u._id === user?._id) { + sendToBottom(); + return; + } + + if (!_isAtBottom()) { + setHasNewMessages(true); + } + }, + callbacks.priority.MEDIUM, + room._id, + ); + + return () => { + callbacks.remove('streamNewMessage', room._id); + }; + }, [_isAtBottom, room._id, sendToBottom, user?._id]); + + useEffect(() => { + const messageList = wrapperRef.current?.querySelector('ul'); + + if (!messageList) { + return; + } + + const observer = new ResizeObserver(() => { + sendToBottomIfNecessary(); + }); + + observer.observe(messageList); + + return () => { + observer?.disconnect(); + }; + }, [sendToBottomIfNecessary]); + + const [routeName] = useCurrentRoute(); + const openedRoom = useSession('openedRoom'); + + useEffect(() => { + Tracker.afterFlush(() => { + if (room._id !== openedRoom) { + return; + } + + if (room && isOmnichannelRoom(room)) { + roomCoordinator.getRoomDirectives(room.t)?.openCustomProfileTab(null, room, room.v.username); + } + }); + }, [openedRoom, room, room._id]); + + const debouncedReadMessageRead = useMemo( + () => + withDebouncing({ wait: 500 })(() => { + readMessage.read(room._id); + }), + [room._id], + ); + + useEffect(() => { + if (!routeName || !roomCoordinator.isRouteNameKnown(routeName) || room._id !== openedRoom) { + return; + } + + debouncedReadMessageRead(); + if (subscribed && (subscription?.alert || subscription?.unread)) { + readMessage.refreshUnreadMark(room._id); + } + }, [debouncedReadMessageRead, openedRoom, room._id, routeName, subscribed, subscription?.alert, subscription?.unread]); + + useEffect(() => { + if (!subscribed) { + setUnreadCount(0); + return; + } + + const count = ChatMessage.find({ + rid: room._id, + ts: { $lte: lastMessage, $gt: subscription?.ls }, + }).count(); + + setUnreadCount(count); + }, [lastMessage, room._id, setUnreadCount, subscribed, subscription?.ls]); + + useEffect(() => { + if (!unread?.count) { + return debouncedReadMessageRead(); + } + readMessage.refreshUnreadMark(room._id); + }, [debouncedReadMessageRead, room._id, unread?.count]); + + useEffect(() => { + const handleReadMessage = (): void => setUnreadCount(0); + readMessage.on(room._id, handleReadMessage); + + return () => { + readMessage.off(room._id, handleReadMessage); + }; + }, [room._id, setUnreadCount]); + + useEffect(() => { + const messageList = wrapperRef.current?.querySelector('ul'); + + if (!messageList) { + return; + } + + const messageEvents: Record<string, (event: any, template: CommonRoomTemplateInstance) => void> = { + ...getCommonRoomEvents(), + 'click .toggle-hidden'(event: JQuery.ClickEvent) { + const mid = event.target.dataset.message; + if (mid) document.getElementById(mid)?.classList.toggle('message--ignored'); + }, + 'load .gallery-item'() { + sendToBottomIfNecessary(); + }, + 'rendered .js-block-wrapper'() { + sendToBottomIfNecessary(); + }, + }; + + const eventHandlers = Object.entries(messageEvents).map(([key, handler]) => { + const [, event, selector] = key.match(/^(.+?)\s(.+)$/) ?? [key, key]; + return { + event, + selector, + listener: (e: JQuery.TriggeredEvent<HTMLUListElement, undefined>) => handler.call(null, e, { data: { rid: room._id, tabBar } }), + }; + }); + + for (const { event, selector, listener } of eventHandlers) { + $(messageList).on(event, selector, listener); + } + + return () => { + for (const { event, selector, listener } of eventHandlers) { + $(messageList).off(event, selector, listener); + } + }; + }, [room._id, sendToBottomIfNecessary, tabBar]); + + useEffect(() => { + const wrapper = wrapperRef.current; + + if (!wrapper) { + return; + } + + const getElementFromPoint = (topOffset = 0): Element | undefined => { + const messagesBox = messagesBoxRef.current; + + if (!messagesBox) { + return; + } + + const messagesBoxLeft = messagesBox.getBoundingClientRect().left + window.pageXOffset; + const messagesBoxTop = messagesBox.getBoundingClientRect().top + window.pageYOffset; + const messagesBoxWidth = parseFloat(getComputedStyle(messagesBox).width); + + let element; + if (document.dir === 'rtl') { + element = document.elementFromPoint(messagesBoxLeft + messagesBoxWidth - 1, messagesBoxTop + topOffset + 1); + } else { + element = document.elementFromPoint(messagesBoxLeft + 1, messagesBoxTop + topOffset + 1); + } + + if (element?.classList.contains('message')) { + return element; + } + }; + + const updateUnreadCount = withThrottling({ wait: 300 })(() => { + Tracker.afterFlush(() => { + const lastInvisibleMessageOnScreen = getElementFromPoint(0) || getElementFromPoint(20) || getElementFromPoint(40); + + if (!lastInvisibleMessageOnScreen || !lastInvisibleMessageOnScreen.id) { + setUnreadCount(0); + return; + } + + const lastMessage = ChatMessage.findOne(lastInvisibleMessageOnScreen.id); + if (!lastMessage) { + setUnreadCount(0); + return; + } + + setLastMessage(lastMessage.ts); + }); + }); + + const handleWrapperScroll = withThrottling({ wait: 100 })((event) => { + const roomLeader = messagesBoxRef.current?.querySelector('.room-leader'); + if (roomLeader) { + if (event.target.scrollTop < lastScrollTopRef.current) { + setHideLeaderHeader(false); + } else if (_isAtBottom(100) === false && event.target.scrollTop > parseFloat(getComputedStyle(roomLeader).height)) { + setHideLeaderHeader(true); + } + } + lastScrollTopRef.current = event.target.scrollTop; + const height = event.target.clientHeight; + const isLoading = RoomHistoryManager.isLoading(room._id); + const hasMore = RoomHistoryManager.hasMore(room._id); + const hasMoreNext = RoomHistoryManager.hasMoreNext(room._id); + + if ((isLoading === false && hasMore === true) || hasMoreNext === true) { + if (hasMore === true && lastScrollTopRef.current <= height / 3) { + RoomHistoryManager.getMore(room._id); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef.current) >= event.target.scrollHeight - height) { + RoomHistoryManager.getMoreNext(room._id, atBottomRef); + } + } + }); + + wrapper.addEventListener('scroll', updateUnreadCount); + wrapper.addEventListener('scroll', handleWrapperScroll); + + return () => { + wrapper.removeEventListener('scroll', updateUnreadCount); + wrapper.removeEventListener('scroll', handleWrapperScroll); + }; + }, [_isAtBottom, room._id, setUnreadCount]); + + useEffect(() => { + const wrapper = wrapperRef.current; + + if (!wrapper) { + return; + } + + const store = NewRoomManager.getStore(room._id); + + const handleWrapperScroll = withThrottling({ wait: 30 })(() => { + store?.update({ scroll: wrapper.scrollTop, atBottom: isAtBottom(wrapper, 50) }); + }); + + const afterMessageGroup = (): void => { + if (store?.scroll && !store.atBottom) { + wrapper.scrollTop = store.scroll; + } else { + sendToBottom(); + } + wrapper.removeEventListener('MessageGroup', afterMessageGroup); + + wrapper.addEventListener('scroll', handleWrapperScroll); + }; + + wrapper.addEventListener('MessageGroup', afterMessageGroup); + + return () => { + wrapper.removeEventListener('MessageGroup', afterMessageGroup); + wrapper.removeEventListener('scroll', handleWrapperScroll); + }; + }, [room._id, sendToBottom]); + + useEffect(() => { + const wrapper = wrapperRef.current; + + if (!wrapper) { + return; + } + + const handleWheel = withThrottling({ wait: 100 })(() => { + checkIfScrollIsAtBottom(); + }); + + const handleTouchStart = (): void => { + atBottomRef.current = false; + }; + + let timer1s: ReturnType<typeof setTimeout> | undefined; + let timer2s: ReturnType<typeof setTimeout> | undefined; + + const handleTouchEnd = (): void => { + checkIfScrollIsAtBottom(); + timer1s = setTimeout(() => checkIfScrollIsAtBottom(), 1000); + timer2s = setTimeout(() => checkIfScrollIsAtBottom(), 2000); + }; + + wrapper.addEventListener('mousewheel', handleWheel); + wrapper.addEventListener('wheel', handleWheel); + wrapper.addEventListener('scroll', handleWheel); + wrapper.addEventListener('touchstart', handleTouchStart); + wrapper.addEventListener('touchend', handleTouchEnd); + + return (): void => { + if (timer1s) clearTimeout(timer1s); + if (timer2s) clearTimeout(timer2s); + wrapper.removeEventListener('mousewheel', handleWheel); + wrapper.removeEventListener('wheel', handleWheel); + wrapper.removeEventListener('scroll', handleWheel); + wrapper.removeEventListener('touchstart', handleTouchStart); + wrapper.removeEventListener('touchend', handleTouchEnd); + }; + }, [checkIfScrollIsAtBottom]); + + const handleComposerResize = useCallback((): void => { + sendToBottomIfNecessary(); + }, [sendToBottomIfNecessary]); + + return ( + <> + {!isLayoutEmbedded && room.announcement && <Announcement announcement={room.announcement} announcementDetails={undefined} />} + <div className='main-content-flex'> + <section + className={`messages-container flex-tab-main-content ${admin ? 'admin' : ''}`} + id={`chat-window-${room._id}`} + aria-label={t('Channel')} + onClick={hideFlexTab ? tabBar.close : undefined} + > + <div className='messages-container-wrapper'> + <div className='messages-container-main' {...fileUploadTriggerProps}> + <DropTargetOverlay {...fileUploadOverlayProps} /> + <div className={['container-bars', (unread || uploading.length) && 'show'].filter(isTruthy).join(' ')}> + {unread ? ( + <UnreadMessagesIndicator + count={unread.count} + since={unread.since} + onJumpButtonClick={handleUnreadBarJumpToButtonClick} + onMarkAsReadButtonClick={handleMarkAsReadButtonClick} + /> + ) : null} + {uploading.map((upload) => ( + <UploadProgressIndicator + key={upload.id} + id={upload.id} + name={upload.name} + percentage={upload.percentage} + error={upload.error instanceof Error ? upload.error.message : undefined} + onClose={handleUploadProgressClose} + /> + ))} + </div> + <div + ref={messagesBoxRef} + className={['messages-box', messageViewMode, roomLeader && 'has-leader'].filter(isTruthy).join(' ')} + > + <NewMessagesButton visible={hasNewMessages} onClick={handleNewMessageButtonClick} /> + <JumpToRecentMessagesBar visible={hasMoreNext} onClick={handleJumpToRecentButtonClick} /> + {!canPreview ? ( + <div className='content room-not-found error-color'> + <div>{t('You_must_join_to_view_messages_in_this_channel')}</div> + </div> + ) : null} + {roomLeader ? ( + <LeaderBar + name={roomLeader.name} + username={roomLeader.username} + status={roomLeader.status} + statusText={roomLeader.statusText} + visible={!hideLeaderHeader} + onAvatarClick={handleOpenUserCardButtonClick} + /> + ) : null} + <div + ref={wrapperRef} + className={[ + 'wrapper', + hasMoreNext && 'has-more-next', + hideUsernames && 'hide-usernames', + !displayAvatars && 'hide-avatar', + ] + .filter(isTruthy) + .join(' ')} + > + <ul className='messages-list' aria-live='polite'> + {canPreview ? ( + <> + {hasMore ? ( + <li className='load-more'>{isLoading ? <LoadingMessagesIndicator /> : null}</li> + ) : ( + <li className='start color-info-font-color'> + {retentionPolicy ? <RetentionPolicyWarning {...retentionPolicy} /> : null} + <RoomForeword user={user} room={room} /> + </li> + )} + </> + ) : null} + {useLegacyMessageTemplate ? <LegacyMessageTemplateList room={room} /> : <MessageList rid={room._id} />} + {hasMoreNext ? <li className='load-more'>{isLoading ? <LoadingMessagesIndicator /> : null}</li> : null} + </ul> + </div> + </div> + <ComposerContainer + rid={room._id} + subscription={subscription} + chatMessagesInstance={chatMessagesInstance} + onResize={handleComposerResize} + /> + </div> + </div> + </section> + </div> + </> + ); +}; + +export default memo(RoomBody); diff --git a/apps/meteor/client/components/RoomForeword.tsx b/apps/meteor/client/views/room/components/body/RoomForeword.tsx similarity index 54% rename from apps/meteor/client/components/RoomForeword.tsx rename to apps/meteor/client/views/room/components/body/RoomForeword.tsx index 27597800da5..9eabe02f5ff 100644 --- a/apps/meteor/client/components/RoomForeword.tsx +++ b/apps/meteor/client/views/room/components/body/RoomForeword.tsx @@ -1,29 +1,17 @@ -import { IRoom, isVoipRoom, isDirectMessageRoom } from '@rocket.chat/core-typings'; +import { IRoom, isVoipRoom, isDirectMessageRoom, IUser } from '@rocket.chat/core-typings'; import { Avatar, Margins, Flex, Box, Tag } from '@rocket.chat/fuselage'; -import { useUser, useUserRoom, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; -import { Users } from '../../app/models/client'; -import { getUserAvatarURL } from '../../app/utils/client'; -import { VoipRoomForeword } from './voip/room/VoipRoomForeword'; +import UserAvatar from '../../../../components/avatar/UserAvatar'; +import { VoipRoomForeword } from '../../../../components/voip/room/VoipRoomForeword'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -type RoomForewordProps = { _id: IRoom['_id']; rid?: IRoom['_id'] } | { rid: IRoom['_id']; _id?: IRoom['_id'] }; - -const RoomForeword = ({ _id, rid }: RoomForewordProps): ReactElement | null => { - const roomId = _id || rid; - if (!roomId) { - throw new Error('Room id required - RoomForeword'); - } +type RoomForewordProps = { user: IUser | null; room: IRoom }; +const RoomForeword = ({ user, room }: RoomForewordProps): ReactElement | null => { const t = useTranslation(); - const user = useUser(); - const room = useUserRoom(roomId); - - if (!room) { - return null; - } - if (isVoipRoom(room)) { return <VoipRoomForeword room={room} />; } @@ -46,13 +34,9 @@ const RoomForeword = ({ _id, rid }: RoomForewordProps): ReactElement | null => { <Flex.Item grow={1}> <Margins block='x24'> <Avatar.Stack> - {usernames.map((username, index) => { - const user = Users.findOne({ username }, { fields: { avatarETag: 1 } }); - - const avatarUrl = getUserAvatarURL(username, user?.avatarETag); - - return <Avatar key={index} size='x48' title={username} url={avatarUrl} data-username={username} />; - })} + {usernames.map((username, index) => ( + <UserAvatar key={index} size='x48' username={username} /> + ))} </Avatar.Stack> </Margins> </Flex.Item> @@ -62,7 +46,7 @@ const RoomForeword = ({ _id, rid }: RoomForewordProps): ReactElement | null => { <Box is='div' mb='x8' flexGrow={1} display='flex' justifyContent='center'> {usernames.map((username, index) => ( <Margins inline='x4' key={index}> - <Box is='a' href={`/direct/${username}`}> + <Box is='a' href={roomCoordinator.getRouteLink('d', { name: username }) || undefined}> <Tag variant='primary' className='mention-link' data-username={username} medium> {username} </Tag> diff --git a/apps/meteor/client/views/room/components/body/UnreadMessagesIndicator.tsx b/apps/meteor/client/views/room/components/body/UnreadMessagesIndicator.tsx new file mode 100644 index 00000000000..289369a0166 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/UnreadMessagesIndicator.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import { useTimeAgo } from '../../../../hooks/useTimeAgo'; + +type UnreadMessagesIndicatorProps = { + count: number; + since?: Date; + onJumpButtonClick: () => void; + onMarkAsReadButtonClick: () => void; +}; + +const UnreadMessagesIndicator = ({ + count, + since, + onJumpButtonClick, + onMarkAsReadButtonClick, +}: UnreadMessagesIndicatorProps): ReactElement => { + const t = useTranslation(); + const formatTimeAgo = useTimeAgo(); + + return ( + <div className='unread-bar color-primary-action-color background-component-color'> + <button type='button' className='jump-to' onClick={onJumpButtonClick}> + <span className='jump-to-large'>{t('Jump_to_first_unread')}</span> + <span className='jump-to-small'>{t('Jump')}</span> + </button> + <span className='unread-count-since'>{t('S_new_messages_since_s', count, since ? formatTimeAgo(since) : undefined)}</span> + <span className='unread-count'>{t('N_new_messages', count)}</span> + <button type='button' className='mark-read' onClick={onMarkAsReadButtonClick}> + {t('Mark_as_read')} + </button> + </div> + ); +}; + +export default UnreadMessagesIndicator; diff --git a/apps/meteor/client/views/room/components/body/UploadProgressIndicator.tsx b/apps/meteor/client/views/room/components/body/UploadProgressIndicator.tsx new file mode 100644 index 00000000000..484e60398dd --- /dev/null +++ b/apps/meteor/client/views/room/components/body/UploadProgressIndicator.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useCallback } from 'react'; + +import { Uploading } from '../../../../../app/ui/client/lib/fileUpload'; +import ErroredUploadProgressIndicator from './ErroredUploadProgressIndicator'; + +type UploadProgressIndicatorProps = { + id: Uploading['id']; + name: string; + percentage: number; + error?: string; + onClose?: (id: Uploading['id']) => void; +}; + +const UploadProgressIndicator = ({ id, name, percentage, error, onClose }: UploadProgressIndicatorProps): ReactElement => { + const t = useTranslation(); + + const handleCloseClick = useCallback(() => { + onClose?.(id); + }, [id, onClose]); + + if (error) { + return <ErroredUploadProgressIndicator id={id} error={error} onClose={onClose} />; + } + + return ( + <div className='upload-progress color-primary-action-color background-component-color'> + <div className='upload-progress-progress' style={{ width: `${percentage}%` }} /> + <div className='upload-progress-text'> + [{percentage}%] {name} + </div> + <button type='button' className='upload-progress-close' onClick={handleCloseClick}> + {t('Cancel')} + </button> + </div> + ); +}; + +export default UploadProgressIndicator; diff --git a/apps/meteor/client/views/room/components/body/useChatMessages.ts b/apps/meteor/client/views/room/components/body/useChatMessages.ts new file mode 100644 index 00000000000..5b3b6fddc45 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useChatMessages.ts @@ -0,0 +1,27 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { RefObject, useEffect, useMemo } from 'react'; + +import { ChatMessages, chatMessages } from '../../../../../app/ui'; + +export const useChatMessages = (rid: IRoom['_id'], wrapperRef: RefObject<HTMLElement | null>): ChatMessages => { + const chatMessagesInstance = useMemo(() => { + const instance = chatMessages[rid] ?? new ChatMessages(); + chatMessages[rid] = instance; + return instance; + }, [rid]); + + useEffect(() => { + const wrapper = wrapperRef.current; + + if (!wrapper) { + return; + } + + chatMessagesInstance.initializeWrapper(wrapper); + return (): void => { + chatMessagesInstance.onDestroyed?.(rid); + }; + }, [chatMessagesInstance, rid, wrapperRef]); + + return chatMessagesInstance; +}; diff --git a/apps/meteor/client/views/room/components/body/useDropTarget.ts b/apps/meteor/client/views/room/components/body/useDropTarget.ts new file mode 100644 index 00000000000..654e0079f5b --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useDropTarget.ts @@ -0,0 +1,40 @@ +import React, { DragEvent, useCallback, useMemo, useState } from 'react'; + +const hasFilesToUpload = (dataTransfer: DataTransfer): boolean => dataTransfer.types.includes('Files'); + +const hasURLToUpload = (dataTransfer: DataTransfer): boolean => + dataTransfer.types.includes('text/uri-list') && dataTransfer.types.includes('text/html'); + +export const useDropTarget = (): { + triggerProps: { + onDragEnter: (event: React.DragEvent<Element>) => void; + }; + overlayProps: { + visible: boolean; + onDismiss: () => void; + }; +} => { + const [visible, setVisible] = useState(false); + + const onDragEnter = useCallback((event: DragEvent) => { + event.stopPropagation(); + + if (!hasFilesToUpload(event.dataTransfer) && !hasURLToUpload(event.dataTransfer)) { + return; + } + + setVisible(true); + }, []); + + const onDismiss = useCallback(() => { + setVisible(false); + }, []); + + const triggerProps = useMemo(() => ({ onDragEnter }), [onDragEnter]); + const overlayProps = useMemo(() => ({ visible, onDismiss }), [visible, onDismiss]); + + return { + triggerProps, + overlayProps, + }; +}; diff --git a/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts new file mode 100644 index 00000000000..a48aac319aa --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts @@ -0,0 +1,88 @@ +import { IRoom, isRoomFederated } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactNode, useCallback, useMemo } from 'react'; + +import { Users } from '../../../../../app/models/client'; +import { chatMessages, fileUpload } from '../../../../../app/ui/client'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { useDropTarget } from './useDropTarget'; + +export const useFileUploadDropTarget = ( + room: IRoom, +): readonly [ + fileUploadTriggerProps: { + onDragEnter: (event: React.DragEvent<Element>) => void; + }, + fileUploadOverlayProps: { + visible: boolean; + onDismiss: () => void; + enabled: boolean; + reason?: ReactNode; + }, +] => { + const { triggerProps, overlayProps } = useDropTarget(); + + const t = useTranslation(); + + const fileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; + const roomFederated = isRoomFederated(room); + const fileUploadAllowedForUser = useReactiveValue( + useCallback( + () => !roomCoordinator.readOnly(room._id, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })), + [room._id], + ), + ); + + const onFileDrop = useMutableCallback(async (files: File[]) => { + const { input } = chatMessages[room._id]; + if (!input) return; + + const { mime } = await import('../../../../../app/utils/lib/mimeTypes'); + + const uploads = Array.from(files).map((file) => { + Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); + return { + file, + name: file.name, + }; + }); + + fileUpload(uploads, input, { rid: room._id }); + }); + + const allOverlayProps = useMemo(() => { + if (!fileUploadEnabled) { + return { + enabled: false, + reason: t('FileUpload_Disabled'), + ...overlayProps, + } as const; + } + + if (roomFederated) { + return { + enabled: false, + reason: t('FileUpload_Disabled_for_federation'), + ...overlayProps, + } as const; + } + + if (!fileUploadAllowedForUser) { + return { + enabled: false, + reason: t('error-not-allowed'), + ...overlayProps, + } as const; + } + + return { + enabled: true, + onFileDrop, + ...overlayProps, + } as const; + }, [fileUploadAllowedForUser, fileUploadEnabled, onFileDrop, overlayProps, roomFederated, t]); + + return [triggerProps, allOverlayProps] as const; +}; diff --git a/apps/meteor/client/views/room/components/body/useMessageContext.ts b/apps/meteor/client/views/room/components/body/useMessageContext.ts new file mode 100644 index 00000000000..0e897c8db5e --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useMessageContext.ts @@ -0,0 +1,94 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useLayout, usePermission, useSetting, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback, useMemo } from 'react'; + +import { AutoTranslate } from '../../../../../app/autotranslate/client'; +import { Subscriptions, Users } from '../../../../../app/models/client'; +import { createMessageContext } from '../../../../../app/ui-utils/client/lib/messageContext'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useMessageContext = (room: IRoom) => { + const uid = useUserId(); + const user = useReactiveValue(useCallback(() => Users.findOne({ _id: uid }), [uid])); + const rid = room._id; + const subscription = useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }), [rid])); + const { isEmbedded: embeddedLayout, isMobile: mobile } = useLayout(); + const translateLanguage = useReactiveValue(useCallback(() => AutoTranslate.getLanguage(rid), [rid])); + const autoImageLoad = useUserPreference('autoImageLoad'); + const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate'); + const saveMobileBandwidth = useUserPreference('saveMobileBandwidth'); + const collapseMediaByDefault = useUserPreference(user, 'collapseMediaByDefault'); + const hasPermissionDeleteMessage = usePermission('delete-message', rid); + const hasPermissionDeleteOwnMessage = usePermission('delete-own-message'); + const displayRoles = useSetting('UI_DisplayRoles'); + const hideRoles = useUserPreference('hideRoles'); + const useRealName = useSetting('UI_Use_Real_Name'); + const chatopsUsername = useSetting('Chatops_Username'); + const autoTranslateEnabled = useSetting('AutoTranslate_Enabled'); + const allowEditing = useSetting('Message_AllowEditing'); + const blockEditInMinutes = useSetting('Message_AllowEditing_BlockEditInMinutes'); + const showEditedStatus = useSetting('Message_ShowEditedStatus'); + const embed = useSetting('API_Embed'); + const embedDisabledFor = useSetting('API_EmbedDisabledFor'); + const groupingPeriod = useSetting('Message_GroupingPeriod') as number; + + return useMemo( + () => + createMessageContext({ + uid, + user, + rid, + room, + subscription, + instance: () => undefined, + embeddedLayout, + translateLanguage, + autoImageLoad, + useLegacyMessageTemplate, + saveMobileBandwidth: mobile && saveMobileBandwidth, + collapseMediaByDefault, + showreply: true, + showReplyButton: true, + hasPermissionDeleteMessage, + hasPermissionDeleteOwnMessage, + hideRoles: !displayRoles || hideRoles, + UI_Use_Real_Name: useRealName, + Chatops_Username: chatopsUsername, + AutoTranslate_Enabled: autoTranslateEnabled, + Message_AllowEditing: allowEditing, + Message_AllowEditing_BlockEditInMinutes: blockEditInMinutes, + Message_ShowEditedStatus: showEditedStatus, + API_Embed: embed, + API_EmbedDisabledFor: embedDisabledFor, + Message_GroupingPeriod: groupingPeriod * 1000, + }), + [ + allowEditing, + autoImageLoad, + autoTranslateEnabled, + blockEditInMinutes, + chatopsUsername, + collapseMediaByDefault, + displayRoles, + embed, + embedDisabledFor, + embeddedLayout, + groupingPeriod, + hasPermissionDeleteMessage, + hasPermissionDeleteOwnMessage, + hideRoles, + mobile, + rid, + room, + saveMobileBandwidth, + showEditedStatus, + subscription, + translateLanguage, + uid, + useLegacyMessageTemplate, + useRealName, + user, + ], + ); +}; diff --git a/apps/meteor/client/views/room/components/body/useRetentionPolicy.ts b/apps/meteor/client/views/room/components/body/useRetentionPolicy.ts new file mode 100644 index 00000000000..7904f21d45d --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useRetentionPolicy.ts @@ -0,0 +1,104 @@ +import { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; + +const hasRetentionPolicy = (room: IRoom & { retention?: any }): room is IRoomWithRetentionPolicy => + 'retention' in room && room.retention !== undefined; + +type RetentionPolicySettings = { + enabled: boolean; + filesOnly: boolean; + doNotPrunePinned: boolean; + appliesToChannels: boolean; + maxAgeChannels: number; + appliesToGroups: boolean; + maxAgeGroups: number; + appliesToDMs: boolean; + maxAgeDMs: number; +}; + +const isActive = (room: IRoom, { enabled, appliesToChannels, appliesToGroups, appliesToDMs }: RetentionPolicySettings): boolean => { + if (!enabled) { + return false; + } + + if (hasRetentionPolicy(room) && room.retention.enabled !== undefined) { + return room.retention.enabled; + } + + switch (room.t) { + case 'c': + return appliesToChannels; + case 'p': + return appliesToGroups; + case 'd': + return appliesToDMs; + } + + return false; +}; + +const extractFilesOnly = (room: IRoom, { filesOnly }: RetentionPolicySettings): boolean => { + if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { + return room.retention.filesOnly; + } + + return filesOnly; +}; + +const extractExcludePinned = (room: IRoom, { doNotPrunePinned }: RetentionPolicySettings): boolean => { + if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { + return room.retention.excludePinned; + } + + return doNotPrunePinned; +}; + +const getMaxAge = (room: IRoom, { maxAgeChannels, maxAgeGroups, maxAgeDMs }: RetentionPolicySettings): number => { + if (hasRetentionPolicy(room) && room.retention.overrideGlobal) { + return room.retention.maxAge; + } + + if (room.t === 'c') { + return maxAgeChannels; + } + if (room.t === 'p') { + return maxAgeGroups; + } + if (room.t === 'd') { + return maxAgeDMs; + } + + return -Infinity; +}; + +export const useRetentionPolicy = ( + room: IRoom | undefined, +): + | { + filesOnly: boolean; + excludePinned: boolean; + maxAge: number; + } + | undefined => { + const settings = { + enabled: useSetting('RetentionPolicy_Enabled') as boolean, + filesOnly: useSetting('RetentionPolicy_FilesOnly') as boolean, + doNotPrunePinned: useSetting('RetentionPolicy_DoNotPrunePinned') as boolean, + appliesToChannels: useSetting('RetentionPolicy_AppliesToChannels') as boolean, + maxAgeChannels: useSetting('RetentionPolicy_MaxAge_Channels') as number, + appliesToGroups: useSetting('RetentionPolicy_AppliesToGroups') as boolean, + maxAgeGroups: useSetting('RetentionPolicy_MaxAge_Groups') as number, + appliesToDMs: useSetting('RetentionPolicy_AppliesToDMs') as boolean, + maxAgeDMs: useSetting('RetentionPolicy_MaxAge_DMs') as number, + } as const; + + if (!room || !isActive(room, settings)) { + return undefined; + } + + return { + filesOnly: extractFilesOnly(room, settings), + excludePinned: extractExcludePinned(room, settings), + maxAge: getMaxAge(room, settings) * 24 * 60 * 60 * 1000, + }; +}; diff --git a/apps/meteor/client/views/room/components/body/useRoomRoles.ts b/apps/meteor/client/views/room/components/body/useRoomRoles.ts new file mode 100644 index 00000000000..073d5dc1a6c --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useRoomRoles.ts @@ -0,0 +1,58 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { RoomRoles, ChatMessage } from '../../../../../app/models/client'; + +export const useRoomRoles = (rid: IRoom['_id']): void => { + const queryClient = useQueryClient(); + const getRoomRoles = useMethod('getRoomRoles'); + const dispatchToastMessage = useToastMessageDispatch(); + + useEffect(() => { + queryClient + .fetchQuery({ + queryKey: ['room', rid, 'roles'], + queryFn: () => getRoomRoles(rid), + staleTime: 15_000, + }) + .then((results) => { + Array.from(results).forEach(({ _id, ...data }) => { + const { + rid, + u: { _id: uid }, + } = data; + RoomRoles.upsert({ rid, 'u._id': uid }, data); + }); + }) + .catch((error) => { + dispatchToastMessage({ type: 'error', message: error }); + }); + + const rolesObserve = RoomRoles.find({ rid }).observe({ + added: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); + }, + changed: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); + }, + removed: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); + }, + }); + + return (): void => { + rolesObserve.stop(); + }; + }, [dispatchToastMessage, getRoomRoles, queryClient, rid]); +}; diff --git a/apps/meteor/client/views/room/components/body/useUnreadMessages.ts b/apps/meteor/client/views/room/components/body/useUnreadMessages.ts new file mode 100644 index 00000000000..7c4ca98993a --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useUnreadMessages.ts @@ -0,0 +1,32 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'; + +import { RoomManager, RoomHistoryManager } from '../../../../../app/ui-utils/client'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; + +export const useUnreadMessages = ( + room: IRoom, +): readonly [ + data: + | { + count: number; + since: Date; + } + | undefined, + setUnreadCount: Dispatch<SetStateAction<number>>, +] => { + const notLoadedCount = useReactiveValue(useCallback(() => RoomHistoryManager.getRoom(room._id).unreadNotLoaded.get(), [room._id])); + const [loadedCount, setLoadedCount] = useState(0); + + const count = useMemo(() => notLoadedCount + loadedCount, [notLoadedCount, loadedCount]); + + const since = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(room._id)?.unreadSince.get(), [room._id])); + + return useMemo(() => { + if (count && since) { + return [{ count, since }, setLoadedCount]; + } + + return [undefined, setLoadedCount]; + }, [count, since]); +}; diff --git a/apps/meteor/client/views/room/index.ts b/apps/meteor/client/views/room/index.ts new file mode 100644 index 00000000000..9ae7a3efb83 --- /dev/null +++ b/apps/meteor/client/views/room/index.ts @@ -0,0 +1,6 @@ +import { lazy } from 'react'; + +export { default as RoomSkeleton } from './RoomSkeleton'; +export const RoomProvider = lazy(() => import('./providers/RoomProvider')); +export const Room = lazy(() => import('./Room/Room')); +export const RoomNotFound = lazy(() => import('./RoomNotFound')); diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx new file mode 100644 index 00000000000..ecf6be29c0e --- /dev/null +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -0,0 +1,28 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { ComponentProps, ReactElement, ReactNode } from 'react'; + +import VerticalBar from '../../../components/VerticalBar/VerticalBar'; + +type RoomLayoutProps = { + header?: ReactNode; + body?: ReactNode; + footer?: ReactNode; + aside?: ReactNode; +} & ComponentProps<typeof Box>; + +const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => ( + <Box is='main' h='full' display='flex' flexDirection='column' {...props}> + {header} + <Box display='flex' flexGrow={1} overflow='hidden' height='full' position='relative'> + <Box display='flex' flexDirection='column' flexGrow={1}> + <Box is='div' display='flex' flexDirection='column' flexGrow={1}> + {body} + </Box> + {footer && <Box is='footer'>{footer}</Box>} + </Box> + {aside && <VerticalBar is='aside'>{aside}</VerticalBar>} + </Box> + </Box> +); + +export default RoomLayout; diff --git a/apps/meteor/client/views/room/lib/Toolbox/index.tsx b/apps/meteor/client/views/room/lib/Toolbox/index.tsx index 943fdd44efc..39e8501174a 100644 --- a/apps/meteor/client/views/room/lib/Toolbox/index.tsx +++ b/apps/meteor/client/views/room/lib/Toolbox/index.tsx @@ -1,8 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Option, Icon } from '@rocket.chat/fuselage'; import { TranslationKey } from '@rocket.chat/ui-contexts'; -import { FC, LazyExoticComponent, ReactNode, MouseEvent, ComponentProps } from 'react'; +import { ReactNode, MouseEvent, ComponentProps, ComponentType } from 'react'; +import { ToolboxContextValue } from './ToolboxContext'; import { generator, Events as GeneratorEvents } from './generator'; type ToolboxHook = ({ room }: { room: IRoom }) => ToolboxActionConfig | null; @@ -34,7 +35,14 @@ export type ToolboxActionConfig = { 'groups': Array<'group' | 'channel' | 'live' | 'direct' | 'direct_multiple' | 'team' | 'voip'>; 'hotkey'?: string; 'action'?: (e?: MouseEvent<HTMLElement>) => void; - 'template'?: string | FC | LazyExoticComponent<FC<{ rid: string; tabBar: any }>>; + 'template'?: + | string + | ComponentType<{ + tabBar: ToolboxContextValue; + _id: IRoom['_id']; + rid: IRoom['_id']; + teamId: IRoom['teamId']; + }>; }; export type ToolboxAction = ToolboxHook | ToolboxActionConfig; diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index d74037bd95d..d4c1f7720fe 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -6,18 +6,18 @@ import { UserAction } from '../../../../app/ui'; import { RoomManager, useHandleRoom } from '../../../lib/RoomManager'; import { AsyncStatePhase } from '../../../lib/asyncState'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import RoomSkeleton from '../Room/RoomSkeleton'; +import RoomSkeleton from '../RoomSkeleton'; import { RoomContext, RoomContextValue } from '../contexts/RoomContext'; import ToolboxProvider from './ToolboxProvider'; -export type Props = { +type RoomProviderProps = { children: ReactNode; rid: IRoom['_id']; }; const fields = {}; -const RoomProvider = ({ rid, children }: Props): JSX.Element => { +const RoomProvider = ({ rid, children }: RoomProviderProps): JSX.Element => { const { phase, value: room } = useHandleRoom(rid); const getMore = useCallback(() => { diff --git a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx index 58cf23eed12..647e18f221f 100644 --- a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx @@ -3,7 +3,13 @@ import { useDebouncedState, useMutableCallback, useSafely } from '@rocket.chat/f import { useSession, useCurrentRoute, useRoute, useUserId, useSetting } from '@rocket.chat/ui-contexts'; import React, { ReactNode, useContext, useMemo, useState, useLayoutEffect, useEffect } from 'react'; -import { removeTabBarContext, setTabBarContext, ToolboxContext, ToolboxEventHandler } from '../lib/Toolbox/ToolboxContext'; +import { + removeTabBarContext, + setTabBarContext, + ToolboxContext, + ToolboxContextValue, + ToolboxEventHandler, +} from '../lib/Toolbox/ToolboxContext'; import { Store } from '../lib/Toolbox/generator'; import { ToolboxAction, ToolboxActionConfig } from '../lib/Toolbox/index'; import VirtualAction from './VirtualAction'; @@ -125,5 +131,6 @@ export const useTab = (): ToolboxActionConfig | undefined => useContext(ToolboxC export const useTabBarOpen = (): ((actionId: string, context?: string) => void) => useContext(ToolboxContext).open; export const useTabBarClose = (): (() => void) => useContext(ToolboxContext).close; export const useTabBarOpenUserInfo = (): ((username: string) => void) => useContext(ToolboxContext).openUserInfo; +export const useTabBarAPI = (): ToolboxContextValue => useContext(ToolboxContext); export default ToolboxProvider; diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 1ff914a3f22..8ea3e46dd04 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -21,6 +21,7 @@ import type { Logger } from '../app/logger/server'; import type { IBusinessHourBehavior } from '../app/livechat/server/business-hour/AbstractBusinessHour'; import { getRandomId } from './random'; import type { ILoginAttempt } from '../app/authentication/server/ILoginAttempt'; +import { compareByRanking } from './utils/comparisons'; enum CallbackPriority { HIGH = -1000, @@ -358,8 +359,7 @@ class Callbacks { stack: new Error().stack, }), ); - const rank = (callback: Callback): number => callback.priority ?? this.priority.MEDIUM; - callbacks.sort((a, b) => rank(a) - rank(b)); + callbacks.sort(compareByRanking((callback: Callback): number => callback.priority ?? this.priority.MEDIUM)); this.setCallbacks(hook, callbacks); } diff --git a/apps/meteor/lib/utils/comparisons.ts b/apps/meteor/lib/utils/comparisons.ts new file mode 100644 index 00000000000..0e528ea32cf --- /dev/null +++ b/apps/meteor/lib/utils/comparisons.ts @@ -0,0 +1,30 @@ +export const compareByRanking = + <T>(rank: (x: T) => number) => + (a: T, b: T) => { + return rank(a) - rank(b); + }; + +export const maxByRanking = + <T>(rank: (x: T) => number) => + (...xs: T[]) => + xs.reduce((a, b) => (rank(a) > rank(b) ? a : b)); + +export const minByRanking = + <T>(rank: (x: T) => number) => + (...xs: T[]) => + xs.reduce((a, b) => (rank(a) < rank(b) ? a : b)); + +const rankDate = (x: Date) => x.getTime(); + +export const compareDates = compareByRanking(rankDate); + +export const maxDate = maxByRanking(rankDate); + +export const minDate = minByRanking(rankDate); + +export const unique = <T>(xs: T[]) => Array.from(new Set(xs)); + +export const difference = <T>(xs: T[], ys: T[]) => { + const set = new Set(ys); + return xs.filter((x) => !set.has(x)); +}; diff --git a/apps/meteor/lib/utils/highOrderFunctions.ts b/apps/meteor/lib/utils/highOrderFunctions.ts new file mode 100644 index 00000000000..47f0b252f05 --- /dev/null +++ b/apps/meteor/lib/utils/highOrderFunctions.ts @@ -0,0 +1,69 @@ +export const withThrottling = + ({ wait, leading, trailing }: { wait: number; leading?: boolean; trailing?: boolean }) => + <TFunction extends (...args: any[]) => any>(fn: TFunction) => { + let timer: ReturnType<typeof setTimeout> | undefined = undefined; + let result: ReturnType<TFunction>; + let previous = 0; + + function throttled(this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) { + const now = Date.now(); + if (!previous && leading === false) previous = now; + const remaining = wait - (now - previous); + + if (remaining <= 0 || remaining > wait) { + if (timer) clearTimeout(timer); + timer = undefined; + previous = now; + result = fn.apply(this, args); + } else if (!timer && trailing !== false) { + const later = () => { + previous = leading === false ? 0 : Date.now(); + result = fn.apply(this, args); + timer = undefined; + }; + timer = setTimeout(later, remaining); + } + return result; + } + + const cancel = () => { + if (timer) clearTimeout(timer); + previous = 0; + timer = undefined; + }; + + return Object.assign(throttled, { cancel }); + }; + +export const withDebouncing = + ({ wait, immediate }: { wait: number; immediate?: boolean }) => + <TFunction extends (...args: any[]) => any>(fn: TFunction) => { + let timer: ReturnType<typeof setTimeout> | undefined = undefined; + let result: ReturnType<TFunction>; + let previous: number; + + function debounced(this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) { + previous = Date.now(); + if (!timer) { + const later = () => { + const passed = Date.now() - previous; + if (wait > passed) { + timer = setTimeout(later, wait - passed); + } else { + timer = undefined; + if (!immediate) result = fn.apply(this, args); + } + }; + timer = setTimeout(later, wait); + if (immediate) result = fn.apply(this, args); + } + return result; + } + + const cancel = () => { + clearTimeout(timer); + timer = undefined; + }; + + return Object.assign(debounced, { cancel }); + }; diff --git a/apps/meteor/lib/utils/omit.ts b/apps/meteor/lib/utils/omit.ts new file mode 100644 index 00000000000..eb13e04dabe --- /dev/null +++ b/apps/meteor/lib/utils/omit.ts @@ -0,0 +1,7 @@ +export function omit<TObject, TKey extends keyof TObject>(obj: TObject, ...keys: TKey[]): Omit<TObject, TKey> { + const result = { ...obj }; + keys.forEach((key) => { + delete result[key]; + }); + return result; +} diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index 01e31bac8ed..e4dd98e7eaf 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -16,6 +16,7 @@ import { logger, authLogger, connLogger } from './Logger'; import type { IConverterOptions } from '../../../app/importer/server/classes/ImportDataConverter'; import { callbacks } from '../../../lib/callbacks'; import { setUserAvatar } from '../../../app/lib/server/functions'; +import { omit } from '../../../lib/utils/omit'; export class LDAPManager { public static async login(username: string, password: string): Promise<LDAPLoginResult> { @@ -268,7 +269,7 @@ export class LDAPManager { ): Promise<IUser | undefined> { logger.debug({ msg: 'Syncing user data', - ldapUser: _.omit(ldapUser, '_raw'), + ldapUser: omit(ldapUser, '_raw'), user: { ...(existingUser && { email: existingUser.emails, _id: existingUser._id }) }, }); diff --git a/apps/meteor/server/lib/logger/logPayloads.ts b/apps/meteor/server/lib/logger/logPayloads.ts index 2edb16a0265..0e9a23a0a07 100644 --- a/apps/meteor/server/lib/logger/logPayloads.ts +++ b/apps/meteor/server/lib/logger/logPayloads.ts @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { omit } from '../../../lib/utils/omit'; const { LOG_METHOD_PAYLOAD = 'false', LOG_REST_PAYLOAD = 'false', LOG_REST_METHOD_PAYLOADS = 'false' } = process.env; @@ -9,7 +9,7 @@ export const getMethodArgs = const params = method === 'ufsWrite' ? args.slice(1) : args; if (method === 'saveSettings') { - return { arguments: [args[0].map((arg: any) => _.omit(arg, 'value'))] }; + return { arguments: [args[0].map((arg: any) => omit(arg, 'value'))] }; } if (method === 'saveSetting') { @@ -17,7 +17,7 @@ export const getMethodArgs = } return { - arguments: params.map((arg) => (typeof arg !== 'object' ? arg : _.omit(arg, 'password', 'msg', 'pass', 'username', 'message'))), + arguments: params.map((arg) => (typeof arg !== 'object' ? arg : omit(arg, 'password', 'msg', 'pass', 'username', 'message'))), }; }; diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 1559a5640b6..66e6705a6f8 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -145,6 +145,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn _id, token: guest.token, status, + username: guest.username, phone: guest?.phone?.[0]?.phoneNumber, }, servedBy: { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index a1efadac905..0142b3922c5 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -164,11 +164,9 @@ export class HomeContent { return data; }, contract); - await this.page.dispatchEvent( - 'div.dropzone-overlay.dropzone-overlay--enabled.background-transparent-darkest.color-content-background-color', - 'drop', - { dataTransfer }, - ); + await this.inputMessage.dispatchEvent('dragenter', { dataTransfer }); + + await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer }); } async openLastMessageMenu(): Promise<void> { diff --git a/packages/core-typings/src/IInquiry.ts b/packages/core-typings/src/IInquiry.ts index bc53ca7c685..ce1aa6b89f1 100644 --- a/packages/core-typings/src/IInquiry.ts +++ b/packages/core-typings/src/IInquiry.ts @@ -18,7 +18,8 @@ export interface IVisitor { _id: string; username: string; token: string; - status: string; + status: 'online' | 'busy' | 'away' | 'offline'; + phone?: string | null; } export interface ILivechatInquiryRecord extends IRocketChatRecord { diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index f80b5c95cb7..2e1c314232d 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -2,6 +2,7 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IMessage } from './IMessage'; import type { IUser, Username } from './IUser'; import type { RoomType } from './RoomType'; +import type { IVisitor } from './IInquiry'; type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing'; @@ -130,11 +131,7 @@ export enum OmnichannelSourceType { export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast' | ''> { t: 'l' | 'v'; - v: { - _id?: string; - token?: string; - status: 'online' | 'busy' | 'away' | 'offline'; - }; + v: IVisitor; email?: { // Data used when the room is created from an email, via email Integration. inbox: string; @@ -209,12 +206,7 @@ export interface IVoipRoom extends IOmnichannelGenericRoom { queue: string; // The ID assigned to the call (opaque ID) callUniqueId?: string; - v: { - _id?: string; - token?: string; - status: 'online' | 'busy' | 'away' | 'offline'; - phone?: string | null; - }; + v: IVisitor; // Outbound means the call was initiated from Rocket.Chat and vise versa direction: 'inbound' | 'outbound'; } -- GitLab From 7d35b72ff86df4177b41cda2bd6774f35b462768 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat> Date: Fri, 9 Sep 2022 21:46:54 -0300 Subject: [PATCH 022/107] Chore: Configure Prettier for `@rocket.chat/livechat` (#26846) --- packages/livechat/.eslintrc.json | 8 +- packages/livechat/package.json | 3 + .../livechat/src/components/Alert/index.js | 13 +- .../livechat/src/components/Alert/stories.js | 1 - packages/livechat/src/components/App/App.js | 36 +- packages/livechat/src/components/App/index.js | 59 +- .../livechat/src/components/Avatar/index.js | 14 +- .../livechat/src/components/Avatar/stories.js | 1 - .../livechat/src/components/Button/index.js | 100 +- .../livechat/src/components/Button/stories.js | 1 - .../src/components/ButtonGroup/index.js | 2 - .../src/components/ButtonGroup/stories.js | 25 +- .../src/components/Calls/CallIFrame.js | 8 +- .../src/components/Calls/CallNotification.js | 55 +- .../src/components/Calls/CallStatus.js | 7 +- .../src/components/Calls/JoinCallButton.js | 30 +- .../Composer/ComposerAction/index.js | 1 - .../Composer/ComposerActions/index.js | 6 +- .../livechat/src/components/Composer/index.js | 44 +- .../src/components/Composer/stories.js | 15 +- .../livechat/src/components/Emoji/ascii.js | 121 +- .../livechat/src/components/Emoji/emojis.js | 4630 ++++++++++++++++- .../components/Emoji/shortnameToUnicode.js | 10 +- .../src/components/FilesDropTarget/index.js | 32 +- .../src/components/FilesDropTarget/stories.js | 1 - .../livechat/src/components/Footer/index.js | 10 +- .../livechat/src/components/Footer/stories.js | 13 +- .../src/components/Form/DateInput/index.js | 13 +- .../src/components/Form/FormField/index.js | 29 +- .../src/components/Form/FormField/stories.js | 1 - .../components/Form/PasswordInput/index.js | 14 +- .../components/Form/PasswordInput/stories.js | 1 - .../src/components/Form/SelectInput/index.js | 27 +- .../components/Form/SelectInput/stories.js | 1 - .../src/components/Form/TextInput/index.js | 28 +- .../src/components/Form/TextInput/stories.js | 1 - .../livechat/src/components/Form/index.js | 1 - .../livechat/src/components/Form/stories.js | 31 +- .../livechat/src/components/Header/index.js | 29 +- .../livechat/src/components/Header/stories.js | 80 +- .../livechat/src/components/Menu/index.js | 33 +- .../livechat/src/components/Menu/stories.js | 37 +- .../Messages/AudioAttachment/index.js | 20 +- .../Messages/AudioAttachment/stories.js | 7 +- .../Messages/FileAttachment/index.js | 21 +- .../Messages/FileAttachment/stories.js | 54 +- .../Messages/FileAttachmentIcon/index.js | 14 +- .../Messages/ImageAttachment/index.js | 18 +- .../Messages/ImageAttachment/stories.js | 6 +- .../src/components/Messages/Message/index.js | 163 +- .../components/Messages/Message/stories.js | 43 +- .../Messages/MessageAvatars/index.js | 18 +- .../Messages/MessageAvatars/stories.js | 25 +- .../Messages/MessageBlocks/index.js | 54 +- .../Messages/MessageBlocks/stories.js | 13 +- .../Messages/MessageBubble/index.js | 20 +- .../Messages/MessageBubble/stories.js | 25 +- .../Messages/MessageContainer/index.js | 18 +- .../Messages/MessageContent/index.js | 6 +- .../components/Messages/MessageList/index.js | 58 +- .../Messages/MessageList/stories.js | 57 +- .../Messages/MessageSeparator/index.js | 32 +- .../Messages/MessageSeparator/stories.js | 22 +- .../components/Messages/MessageText/emoji.js | 4 +- .../components/Messages/MessageText/index.js | 7 +- .../Messages/MessageText/markdown.js | 3 +- .../components/Messages/MessageTime/index.js | 1 - .../Messages/MessageTime/stories.js | 9 +- .../components/Messages/TypingDots/index.js | 12 +- .../components/Messages/TypingDots/stories.js | 5 +- .../Messages/TypingIndicator/index.js | 13 +- .../Messages/TypingIndicator/stories.js | 7 +- .../Messages/VideoAttachment/index.js | 20 +- .../Messages/VideoAttachment/stories.js | 7 +- .../src/components/Modal/component.js | 58 +- .../livechat/src/components/Modal/manager.js | 1 - .../livechat/src/components/Modal/stories.js | 21 +- .../livechat/src/components/Popover/index.js | 13 +- .../src/components/Popover/stories.js | 29 +- .../livechat/src/components/Screen/Header.js | 36 +- .../livechat/src/components/Screen/index.js | 76 +- .../livechat/src/components/Screen/stories.js | 45 +- .../livechat/src/components/Sound/index.js | 10 +- .../livechat/src/components/Sound/stories.js | 15 +- .../livechat/src/components/Tooltip/index.js | 72 +- .../src/components/Tooltip/stories.js | 9 +- packages/livechat/src/components/helpers.js | 61 +- .../uiKit/message/ActionsBlock/index.js | 38 +- .../src/components/uiKit/message/Block.js | 47 +- .../uiKit/message/ButtonElement/index.js | 63 +- .../uiKit/message/ButtonElement/stories.js | 19 +- .../uiKit/message/ContextBlock/index.js | 10 +- .../uiKit/message/DatePickerElement/index.js | 41 +- .../uiKit/message/DividerBlock/index.js | 9 +- .../uiKit/message/ImageBlock/index.js | 75 +- .../uiKit/message/ImageElement/index.js | 7 +- .../components/uiKit/message/Mrkdwn/index.js | 5 +- .../uiKit/message/OverflowElement/index.js | 78 +- .../uiKit/message/PlainText/index.js | 12 +- .../uiKit/message/SectionBlock/index.js | 19 +- .../message/StaticSelectElement/index.js | 63 +- .../src/components/uiKit/message/Surface.js | 8 +- .../uiKit/message/actions.stories.js | 20 +- .../uiKit/message/context.stories.js | 4 +- .../uiKit/message/divider.stories.js | 4 +- .../components/uiKit/message/image.stories.js | 4 +- .../src/components/uiKit/message/index.js | 3 +- .../uiKit/message/section.stories.js | 16 +- packages/livechat/src/helpers.stories.js | 28 +- packages/livechat/src/icons/stories.js | 14 +- packages/livechat/src/lib/api.js | 11 +- packages/livechat/src/lib/email.js | 3 +- packages/livechat/src/lib/hooks.js | 31 +- packages/livechat/src/lib/locale.js | 71 +- packages/livechat/src/lib/main.js | 17 +- packages/livechat/src/lib/parentCall.js | 3 +- packages/livechat/src/lib/random.js | 7 +- packages/livechat/src/lib/room.js | 7 +- packages/livechat/src/lib/threads.js | 23 +- packages/livechat/src/lib/transcript.js | 26 +- packages/livechat/src/lib/triggers.js | 20 +- packages/livechat/src/lib/uiKit.js | 13 +- packages/livechat/src/lib/userPresence.js | 1 - .../livechat/src/routes/Chat/component.js | 271 +- .../livechat/src/routes/Chat/connector.js | 60 +- .../livechat/src/routes/Chat/container.js | 106 +- packages/livechat/src/routes/Chat/stories.js | 16 +- .../src/routes/ChatFinished/component.js | 29 +- .../src/routes/ChatFinished/container.js | 25 +- .../src/routes/GDPRAgreement/component.js | 59 +- .../src/routes/GDPRAgreement/container.js | 23 +- .../src/routes/GDPRAgreement/stories.js | 1 - .../src/routes/LeaveMessage/component.js | 180 +- .../src/routes/LeaveMessage/container.js | 32 +- .../src/routes/LeaveMessage/stories.js | 1 - .../livechat/src/routes/Register/component.js | 187 +- .../livechat/src/routes/Register/container.js | 32 +- .../livechat/src/routes/Register/stories.js | 3 +- .../src/routes/SwitchDepartment/component.js | 39 +- .../src/routes/SwitchDepartment/connector.js | 16 +- .../src/routes/SwitchDepartment/container.js | 8 +- .../src/routes/SwitchDepartment/stories.js | 1 - .../src/routes/TriggerMessage/component.js | 21 +- .../src/routes/TriggerMessage/container.js | 45 +- .../src/routes/TriggerMessage/stories.js | 11 +- packages/livechat/src/store/index.js | 9 +- packages/livechat/src/widget.js | 102 +- 147 files changed, 6531 insertions(+), 2366 deletions(-) diff --git a/packages/livechat/.eslintrc.json b/packages/livechat/.eslintrc.json index c3bb192b544..3ef2b2023dd 100644 --- a/packages/livechat/.eslintrc.json +++ b/packages/livechat/.eslintrc.json @@ -1,11 +1,12 @@ { - "extends": ["@rocket.chat/eslint-config"], - "plugins": ["react", "react-hooks"], + "extends": ["@rocket.chat/eslint-config", "prettier"], + "plugins": ["react", "react-hooks", "prettier"], "parser": "@babel/eslint-parser", "env": { "jest": true }, "rules": { + "prettier/prettier": "error", "import/order": ["error", { "newlines-between": "always", "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], @@ -45,7 +46,8 @@ "react/self-closing-comp": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", - "no-sequences": "off" + "no-sequences": "off", + "no-extra-parens": "off" }, "settings": { "import/resolver": { diff --git a/packages/livechat/package.json b/packages/livechat/package.json index edfdebcf196..8fbe54e8bcd 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -108,6 +108,9 @@ "last 2 versions", "not ie < 11" ], + "volta": { + "extends": "../../package.json" + }, "houston": { "updateFiles": [ "package.json" diff --git a/packages/livechat/src/components/Alert/index.js b/packages/livechat/src/components/Alert/index.js index 14375ffcfe4..35448684cb8 100644 --- a/packages/livechat/src/components/Alert/index.js +++ b/packages/livechat/src/components/Alert/index.js @@ -5,7 +5,6 @@ import CloseIcon from '../../icons/close.svg'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - class Alert extends Component { static defaultProps = { timeout: 3000, @@ -34,18 +33,12 @@ class Alert extends Component { className={createClassName(styles, 'alert', { success, warning, error }, [className])} style={{ ...style, - ...color && { backgroundColor: color }, + ...(color && { backgroundColor: color }), }} > - <div className={createClassName(styles, 'alert__content')}> - {children} - </div> + <div className={createClassName(styles, 'alert__content')}>{children}</div> {!hideCloseButton && ( - <button - onClick={this.handleDismiss} - className={createClassName(styles, 'alert__close')} - aria-label={t('dismiss_this_alert')} - > + <button onClick={this.handleDismiss} className={createClassName(styles, 'alert__close')} aria-label={t('dismiss_this_alert')}> <CloseIcon width={20} height={20} /> </button> )} diff --git a/packages/livechat/src/components/Alert/stories.js b/packages/livechat/src/components/Alert/stories.js index 31fffbd78b9..10dd0044696 100644 --- a/packages/livechat/src/components/Alert/stories.js +++ b/packages/livechat/src/components/Alert/stories.js @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import Alert from '.'; import { screenCentered, loremIpsum } from '../../helpers.stories'; - storiesOf('Components/Alert', module) .addDecorator(withKnobs) .addDecorator(screenCentered) diff --git a/packages/livechat/src/components/App/App.js b/packages/livechat/src/components/App/App.js index 957f882d134..91accd921c4 100644 --- a/packages/livechat/src/components/App/App.js +++ b/packages/livechat/src/components/App/App.js @@ -23,7 +23,7 @@ import { visibility, isActiveSession, setInitCookies } from '../helpers'; function isRTL(s) { const rtlChars = '\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC'; - const rtlDirCheck = new RegExp(`^[^${ rtlChars }]*?[${ rtlChars }]`); + const rtlDirCheck = new RegExp(`^[^${rtlChars}]*?[${rtlChars}]`); return rtlDirCheck.test(s); } @@ -47,9 +47,7 @@ export class App extends Component { online, departments = [], }, - gdpr: { - accepted: gdprAccepted, - }, + gdpr: { accepted: gdprAccepted }, triggered, user, } = this.props; @@ -67,12 +65,11 @@ export class App extends Component { const showDepartment = departments.filter((dept) => dept.showOnRegistration).length > 0; - const showRegistrationForm = ( - registrationForm - && (nameFieldRegistrationForm || emailFieldRegistrationForm || showDepartment) - ) - && !triggered - && !(user && user.token); + const showRegistrationForm = + registrationForm && + (nameFieldRegistrationForm || emailFieldRegistrationForm || showDepartment) && + !triggered && + !(user && user.token); if (showRegistrationForm) { return route('/register'); } @@ -80,7 +77,9 @@ export class App extends Component { }; handleTriggers() { - const { config: { online, enabled } } = this.props; + const { + config: { online, enabled }, + } = this.props; if (online && enabled) { Triggers.init(); } @@ -143,7 +142,11 @@ export class App extends Component { dismissNotification = () => !isActiveSession(); initWidget() { - const { minimized, iframe: { visible }, dispatch } = this.props; + const { + minimized, + iframe: { visible }, + dispatch, + } = this.props; parentCall(minimized ? 'minimizeWindow' : 'restoreWindow'); parentCall(visible ? 'showWidget' : 'hideWidget'); @@ -202,14 +205,7 @@ export class App extends Component { } } - render = ({ - sound, - undocked, - minimized, - expanded, - alerts, - modal, - }, { initialized, poppedOut }) => { + render = ({ sound, undocked, minimized, expanded, alerts, modal }, { initialized, poppedOut }) => { if (!initialized) { return null; } diff --git a/packages/livechat/src/components/App/index.js b/packages/livechat/src/components/App/index.js index 1a71971f048..7fd13a4943e 100644 --- a/packages/livechat/src/components/App/index.js +++ b/packages/livechat/src/components/App/index.js @@ -1,39 +1,28 @@ import { Provider as StoreProvider, Consumer as StoreConsumer } from '../../store'; import App from './App'; -const AppConnector = () => <div id='app'> - <StoreProvider> - <StoreConsumer> - {({ - config, - user, - triggered, - gdpr, - sound, - undocked, - minimized = true, - expanded = false, - alerts, - modal, - dispatch, - iframe, - }) => ( - <App - config={config} - gdpr={gdpr} - triggered={triggered} - user={user} - sound={sound} - undocked={undocked} - minimized={minimized} - expanded={expanded} - alerts={alerts} - modal={modal} - dispatch={dispatch} - iframe={iframe} - /> - )} - </StoreConsumer> - </StoreProvider> -</div>; +const AppConnector = () => ( + <div id='app'> + <StoreProvider> + <StoreConsumer> + {({ config, user, triggered, gdpr, sound, undocked, minimized = true, expanded = false, alerts, modal, dispatch, iframe }) => ( + <App + config={config} + gdpr={gdpr} + triggered={triggered} + user={user} + sound={sound} + undocked={undocked} + minimized={minimized} + expanded={expanded} + alerts={alerts} + modal={modal} + dispatch={dispatch} + iframe={iframe} + /> + )} + </StoreConsumer> + </StoreProvider> + </div> +); export default AppConnector; diff --git a/packages/livechat/src/components/Avatar/index.js b/packages/livechat/src/components/Avatar/index.js index 72a9d553e89..a2618b44973 100644 --- a/packages/livechat/src/components/Avatar/index.js +++ b/packages/livechat/src/components/Avatar/index.js @@ -3,7 +3,6 @@ import { Component } from 'preact'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - export class Avatar extends Component { static getDerivedStateFromProps(props) { if (props.src) { @@ -27,18 +26,11 @@ export class Avatar extends Component { className={createClassName(styles, 'avatar', { small, large, nobg: src && !errored }, [className])} style={style} > - {(src && !errored) && ( - <img - src={src} - alt={description} - className={createClassName(styles, 'avatar__image')} - onError={this.handleError} - /> + {src && !errored && ( + <img src={src} alt={description} className={createClassName(styles, 'avatar__image')} onError={this.handleError} /> )} - {status && ( - <span className={createClassName(styles, 'avatar__status', { small, large, status })} /> - )} + {status && <span className={createClassName(styles, 'avatar__status', { small, large, status })} />} </div> ); } diff --git a/packages/livechat/src/components/Avatar/stories.js b/packages/livechat/src/components/Avatar/stories.js index 46054035c68..0cdb8730a02 100644 --- a/packages/livechat/src/components/Avatar/stories.js +++ b/packages/livechat/src/components/Avatar/stories.js @@ -4,7 +4,6 @@ import { storiesOf } from '@storybook/react'; import { Avatar } from '.'; import { avatarResolver, centered } from '../../helpers.stories'; - const defaultSrc = avatarResolver('guilherme.gazzo'); const defaultDescription = 'user description'; const statuses = [null, 'offline', 'away', 'busy', 'online']; diff --git a/packages/livechat/src/components/Button/index.js b/packages/livechat/src/components/Button/index.js index 6f1660036bd..f1c9f918f67 100644 --- a/packages/livechat/src/components/Button/index.js +++ b/packages/livechat/src/components/Button/index.js @@ -1,50 +1,60 @@ import { createClassName, memo } from '../helpers'; import styles from './styles.scss'; - const handleMouseUp = ({ target }) => target.blur(); -export const Button = memo(({ - submit, - disabled, - outline, - nude, - danger, - secondary, - stack, - small, - loading, - badge, - icon, - onClick, - className, - style = {}, - children, - img, -}) => ( - <button - type={submit ? 'submit' : 'button'} - disabled={disabled} - onClick={onClick} - onMouseUp={handleMouseUp} - aria-label={icon ? children[0] : null} - className={createClassName(styles, 'button', { - disabled, - outline, - nude, - danger, - secondary, - stack, - small, - loading, - icon: !!icon, - img, - }, [className])} - style={Object.assign({}, style, img && { - backgroundImage: `url(${ img })`, - })} - > - {badge ? (<span className={createClassName(styles, 'button__badge')}>{badge}</span>) : null} - {!img && (icon || children)} - </button> -)); +export const Button = memo( + ({ + submit, + disabled, + outline, + nude, + danger, + secondary, + stack, + small, + loading, + badge, + icon, + onClick, + className, + style = {}, + children, + img, + }) => ( + <button + type={submit ? 'submit' : 'button'} + disabled={disabled} + onClick={onClick} + onMouseUp={handleMouseUp} + aria-label={icon ? children[0] : null} + className={createClassName( + styles, + 'button', + { + disabled, + outline, + nude, + danger, + secondary, + stack, + small, + loading, + icon: !!icon, + img, + }, + [className], + )} + style={Object.assign( + {}, + style, + img && { + backgroundImage: `url(${img})`, + }, + )} + > + {badge ? <span className={createClassName(styles, 'button__badge')}>{badge}</span> : null} + {!img && (icon || children)} + </button> + ), +); diff --git a/packages/livechat/src/components/Button/stories.js b/packages/livechat/src/components/Button/stories.js index 6e59f2f8b3e..cfe5e09c9a4 100644 --- a/packages/livechat/src/components/Button/stories.js +++ b/packages/livechat/src/components/Button/stories.js @@ -6,7 +6,6 @@ import { Button } from '.'; import { avatarResolver, centered } from '../../helpers.stories'; import ChatIcon from '../../icons/chat.svg'; - const defaultSrc = avatarResolver('guilherme.gazzo'); const defaultText = 'Powered by Rocket.Chat'; diff --git a/packages/livechat/src/components/ButtonGroup/index.js b/packages/livechat/src/components/ButtonGroup/index.js index 120fe1aff4e..cfbbde4b70f 100644 --- a/packages/livechat/src/components/ButtonGroup/index.js +++ b/packages/livechat/src/components/ButtonGroup/index.js @@ -1,10 +1,8 @@ import { cloneElement, toChildArray } from 'preact'; - import { createClassName, memo } from '../helpers'; import styles from './styles.scss'; - export const ButtonGroup = memo(({ children }) => ( <div className={createClassName(styles, 'button-group')}> {toChildArray(children).map((child) => cloneElement(child, { className: createClassName(styles, 'button-group__item') }))} diff --git a/packages/livechat/src/components/ButtonGroup/stories.js b/packages/livechat/src/components/ButtonGroup/stories.js index 8238132c267..72cf34c61cc 100644 --- a/packages/livechat/src/components/ButtonGroup/stories.js +++ b/packages/livechat/src/components/ButtonGroup/stories.js @@ -5,7 +5,6 @@ import { ButtonGroup } from '.'; import { centered } from '../../helpers.stories'; import { Button } from '../Button'; - storiesOf('Components/ButtonGroup', module) .addDecorator(centered) .addDecorator(withKnobs) @@ -20,20 +19,32 @@ storiesOf('Components/ButtonGroup', module) <ButtonGroup> <Button small>{text('button text 1', 'Yes')}</Button> <Button outline>{text('button text 2', 'Cancel')}</Button> - <Button small danger>{text('button text 3', 'No')}</Button> + <Button small danger> + {text('button text 3', 'No')} + </Button> </ButtonGroup> )) .add('with only small buttons', () => ( <ButtonGroup> <Button small>{text('button text 1', 'Yes')}</Button> - <Button small outline>{text('button text 2', 'Cancel')}</Button> - <Button small danger>{text('button text 3', 'No')}</Button> + <Button small outline> + {text('button text 2', 'Cancel')} + </Button> + <Button small danger> + {text('button text 3', 'No')} + </Button> </ButtonGroup> )) .add('with stacked buttons', () => ( <ButtonGroup> - <Button small outline>Rename</Button> - <Button small outline>Share</Button> - <Button stack danger>Delete</Button> + <Button small outline> + Rename + </Button> + <Button small outline> + Share + </Button> + <Button stack danger> + Delete + </Button> </ButtonGroup> )); diff --git a/packages/livechat/src/components/Calls/CallIFrame.js b/packages/livechat/src/components/Calls/CallIFrame.js index 445e0eadbe5..a1549f06779 100644 --- a/packages/livechat/src/components/Calls/CallIFrame.js +++ b/packages/livechat/src/components/Calls/CallIFrame.js @@ -4,16 +4,12 @@ import { createClassName } 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 = `${Livechat.client.host}/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(`${Livechat.client.host}/meet/${room._id}?token=${token}`, room._id); return store.setState({ incomingCallAlert: { ...incomingCallAlert, show: false }, ongoingCall: { diff --git a/packages/livechat/src/components/Calls/CallNotification.js b/packages/livechat/src/components/Calls/CallNotification.js index e4fa4d06f6d..80be3182d16 100644 --- a/packages/livechat/src/components/Calls/CallNotification.js +++ b/packages/livechat/src/components/Calls/CallNotification.js @@ -12,22 +12,12 @@ import { createClassName, getAvatarUrl, isMobileDevice } from '../helpers'; import { CallStatus } from './CallStatus'; import styles from './styles.scss'; - -const CallNotification = ({ - callProvider, - callerUsername, - url, - dispatch, - time, - rid, - callId, - t, -}) => { +const CallNotification = ({ callProvider, callerUsername, url, dispatch, time, rid, callId, t }) => { const [show, setShow] = useState(true); const callInNewTab = async () => { const { token } = store.state; - const url = `${ Livechat.client.host }/meet/${ rid }?token=${ token }`; + const url = `${Livechat.client.host}/meet/${rid}?token=${token}`; await dispatch({ ongoingCall: { callStatus: CallStatus.IN_PROGRESS_DIFFERENT_TAB, @@ -81,31 +71,24 @@ const CallNotification = ({ return ( <div className={createClassName(styles, 'call-notification')}> - { - show && ( - <div className = { createClassName(styles, 'call-notification__content') }> - <div className = { createClassName(styles, 'call-notification__content-avatar') }> - <Avatar src = { getAvatarUrl(callerUsername) } large /> - </div> - <div className = { createClassName(styles, 'call-notification__content-message') }> - { t('incoming_video_call') } - </div> - <div className = { createClassName(styles, 'call-notification__content-actions') }> - <Button - onClick = { declineClick } - className = { createClassName(styles, 'call-notification__content-actions-decline') }> - <PhoneDecline width = { 20 } height = { 20 } /> - <span style='margin-left:5px'> {t('decline')} </span > - </Button> - <Button onClick = { acceptClick } - className = {createClassName(styles, 'call-notification__content-actions-accept') }> - <PhoneAccept width = { 20 } height = { 20} /> - <span style='margin-left:5px'> {t('accept')} </span > - </Button> - </div> + {show && ( + <div className={createClassName(styles, 'call-notification__content')}> + <div className={createClassName(styles, 'call-notification__content-avatar')}> + <Avatar src={getAvatarUrl(callerUsername)} large /> </div> - ) - } + <div className={createClassName(styles, 'call-notification__content-message')}>{t('incoming_video_call')}</div> + <div className={createClassName(styles, 'call-notification__content-actions')}> + <Button onClick={declineClick} className={createClassName(styles, 'call-notification__content-actions-decline')}> + <PhoneDecline width={20} height={20} /> + <span style='margin-left:5px'> {t('decline')} </span> + </Button> + <Button onClick={acceptClick} className={createClassName(styles, 'call-notification__content-actions-accept')}> + <PhoneAccept width={20} height={20} /> + <span style='margin-left:5px'> {t('accept')} </span> + </Button> + </div> + </div> + )} </div> ); }; diff --git a/packages/livechat/src/components/Calls/CallStatus.js b/packages/livechat/src/components/Calls/CallStatus.js index d5f4ad85162..f9bc0e07f44 100644 --- a/packages/livechat/src/components/Calls/CallStatus.js +++ b/packages/livechat/src/components/Calls/CallStatus.js @@ -7,6 +7,7 @@ export const CallStatus = { ENDED: 'ended', }; -export const isCallOngoing = (callStatus) => callStatus === CallStatus.IN_PROGRESS - || callStatus === CallStatus.IN_PROGRESS_DIFFERENT_TAB - || callStatus === CallStatus.IN_PROGRESS_SAME_TAB; +export const isCallOngoing = (callStatus) => + callStatus === CallStatus.IN_PROGRESS || + callStatus === CallStatus.IN_PROGRESS_DIFFERENT_TAB || + callStatus === CallStatus.IN_PROGRESS_SAME_TAB; diff --git a/packages/livechat/src/components/Calls/JoinCallButton.js b/packages/livechat/src/components/Calls/JoinCallButton.js index a0ea279480b..b98c8dcae66 100644 --- a/packages/livechat/src/components/Calls/JoinCallButton.js +++ b/packages/livechat/src/components/Calls/JoinCallButton.js @@ -9,7 +9,6 @@ import { createClassName } from '../helpers'; import { isCallOngoing } from './CallStatus'; import styles from './styles.scss'; - export const JoinCallButton = ({ t, ...props }) => { const { token, room } = store.state; @@ -20,30 +19,27 @@ export const JoinCallButton = ({ t, ...props }) => { break; } case constants.webRTCCallStartedMessageType: { - window.open(`${ Livechat.client.host }/meet/${ room._id }?token=${ token }`, room._id); + window.open(`${Livechat.client.host}/meet/${room._id}?token=${token}`, room._id); break; } } }; return ( <div className={createClassName(styles, 'joinCall')}> - { - isCallOngoing(props.callStatus) - && ( - <div> - <div className={createClassName(styles, 'joinCall__content')} > - <div className={createClassName(styles, 'joinCall__content-videoIcon')} > - <VideoIcon width={20} height={20} /> - </div> - { t('join_my_room_to_start_the_video_call') } - </div> - <Button onClick={clickJoinCall} className={createClassName(styles, 'joinCall__content-action')}> + {isCallOngoing(props.callStatus) && ( + <div> + <div className={createClassName(styles, 'joinCall__content')}> + <div className={createClassName(styles, 'joinCall__content-videoIcon')}> <VideoIcon width={20} height={20} /> - {t('join_call')} - </Button> + </div> + {t('join_my_room_to_start_the_video_call')} </div> - ) - } + <Button onClick={clickJoinCall} className={createClassName(styles, 'joinCall__content-action')}> + <VideoIcon width={20} height={20} /> + {t('join_call')} + </Button> + </div> + )} </div> ); }; diff --git a/packages/livechat/src/components/Composer/ComposerAction/index.js b/packages/livechat/src/components/Composer/ComposerAction/index.js index ccd771f03ec..ff971d4ff6b 100644 --- a/packages/livechat/src/components/Composer/ComposerAction/index.js +++ b/packages/livechat/src/components/Composer/ComposerAction/index.js @@ -1,7 +1,6 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - export const ComposerAction = memo(({ text, onClick, className, style = {}, children }) => ( <button type='button' diff --git a/packages/livechat/src/components/Composer/ComposerActions/index.js b/packages/livechat/src/components/Composer/ComposerActions/index.js index 0c952487aef..41e27cc013b 100644 --- a/packages/livechat/src/components/Composer/ComposerActions/index.js +++ b/packages/livechat/src/components/Composer/ComposerActions/index.js @@ -1,12 +1,8 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - export const ComposerActions = memo(({ className, style = {}, children }) => ( - <div - className={createClassName(styles, 'composer__actions', {}, [className])} - style={style} - > + <div className={createClassName(styles, 'composer__actions', {}, [className])} style={style}> {children} </div> )); diff --git a/packages/livechat/src/components/Composer/index.js b/packages/livechat/src/components/Composer/index.js index 9c1fa2aa302..792e0109920 100644 --- a/packages/livechat/src/components/Composer/index.js +++ b/packages/livechat/src/components/Composer/index.js @@ -64,15 +64,15 @@ export class Composer extends Component { const items = Array.from(event.clipboardData.items); - const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)) - .map((item) => item.getAsFile()); + const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)).map((item) => item.getAsFile()); if (files.length) { onUpload && onUpload(files); return; } const texts = await Promise.all( - items.filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type)) + items + .filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type)) .map((item) => new Promise((resolve) => item.getAsString(resolve))), ); @@ -88,15 +88,15 @@ export class Composer extends Component { const items = Array.from(event.dataTransfer.items); - const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)) - .map((item) => item.getAsFile()); + const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)).map((item) => item.getAsFile()); if (files.length) { onUpload && onUpload(files); return; } const texts = await Promise.all( - items.filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type)) + items + .filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type)) .map((item) => new Promise((resolve) => item.getAsString(resolve))), ); texts.forEach((text) => this.pasteText(parse(text))); @@ -176,7 +176,7 @@ export class Composer extends Component { const { onChange } = this.props; const caretPosition = this.getCaretPosition(this.el); const oldText = this.el.innerText; - const newText = `${ oldText.substr(0, caretPosition) }${ emoji } ${ oldText.substr(caretPosition) }`; + const newText = `${oldText.substr(0, caretPosition)}${emoji} ${oldText.substr(caretPosition)}`; this.el.innerHTML = newText; this.moveCursorToEndAndFocus(caretPosition + emoji.length + 1); onChange && onChange(this.el.innerText); @@ -219,34 +219,30 @@ export class Composer extends Component { } render = ({ pre, post, value, placeholder, onChange, onSubmit, onUpload, className, style }) => ( - <div className={createClassName(styles, 'composer', { }, [className])} style={style}> + <div className={createClassName(styles, 'composer', {}, [className])} style={style}> {pre} <div ref={this.handleRef} - {...( - { - contentEditable: true, - 'data-placeholder': placeholder, - onInput: this.handleInput(onChange), - onKeypress: this.handleKeypress(onSubmit), - onPaste: this.handlePaste(onUpload), - onDrop: this.handleDrop(onUpload), - onClick: this.handleClick, - } - )} - + {...{ + 'contentEditable': true, + 'data-placeholder': placeholder, + 'onInput': this.handleInput(onChange), + 'onKeypress': this.handleKeypress(onSubmit), + 'onPaste': this.handlePaste(onUpload), + 'onDrop': this.handleDrop(onUpload), + 'onClick': this.handleClick, + }} onCompositionStart={() => { this.handleInputLock(true); }} - onCompositionEnd={() => { this.handleInputLock(false); onChange && onChange(this.el.innerText); }} - - className={createClassName(styles, 'composer__input')} - >{value}</div> + > + {value} + </div> {post} </div> ); diff --git a/packages/livechat/src/components/Composer/stories.js b/packages/livechat/src/components/Composer/stories.js index 783eecb13ee..76097ce2068 100644 --- a/packages/livechat/src/components/Composer/stories.js +++ b/packages/livechat/src/components/Composer/stories.js @@ -8,12 +8,7 @@ import PlusIcon from '../../icons/plus.svg'; import SendIcon from '../../icons/send.svg'; import SmileIcon from '../../icons/smile.svg'; - -const centeredWithWidth = (storyFn, ...args) => centered(() => ( - <div style={{ width: '365px' }}> - {storyFn()} - </div> -), ...args); +const centeredWithWidth = (storyFn, ...args) => centered(() => <div style={{ width: '365px' }}>{storyFn()}</div>, ...args); const defaultPlaceholder = 'Insert your text here'; @@ -29,9 +24,7 @@ storiesOf('Components/Composer', module) onUpload={action('upload')} /> )) - .add('connecting', () => ( - <Composer connecting /> - )) + .add('connecting', () => <Composer connecting />) .add('with large placeholder', () => ( <Composer value={text('value', '')} @@ -52,7 +45,7 @@ storiesOf('Components/Composer', module) )) .add('with emojis', () => ( <Composer - value={text('value', ':heart: :smile: :\'(')} + value={text('value', ":heart: :smile: :'(")} placeholder={text('placeholder', defaultPlaceholder)} onChange={action('change')} onSubmit={action('submit')} @@ -61,7 +54,7 @@ storiesOf('Components/Composer', module) )) .add('with mentions', () => ( <Composer - value={text('value', '@all, I\'m @here with @user.')} + value={text('value', "@all, I'm @here with @user.")} placeholder={text('placeholder', defaultPlaceholder)} onChange={action('change')} onSubmit={action('submit')} diff --git a/packages/livechat/src/components/Emoji/ascii.js b/packages/livechat/src/components/Emoji/ascii.js index ec50b46a8a8..11674a52d93 100644 --- a/packages/livechat/src/components/Emoji/ascii.js +++ b/packages/livechat/src/components/Emoji/ascii.js @@ -1,5 +1,122 @@ -const ascii = { '*\\0/*': '🙆', '*\\O/*': '🙆', '-___-': '😑', ':\'-)': '😂', '\':-)': '😅', '\':-D': '😅', '>:-)': '😆', '\':-(': '😓', '>:-(': '😠', ':\'-(': '😢', 'O:-)': '😇', '0:-3': '😇', '0:-)': '😇', '0;^)': '😇', 'O;-)': '😇', '0;-)': '😇', 'O:-3': '😇', '-__-': '😑', ':-Þ': '😛', '</3': '💔', ':\')': '😂', ':-D': '😃', '\':)': '😅', '\'=)': '😅', '\':D': '😅', '\'=D': '😅', '>:)': '😆', '>;)': '😆', '>=)': '😆', ';-)': '😉', '*-)': '😉', ';-]': '😉', ';^)': '😉', '\':(': '😓', '\'=(': '😓', ':-*': '😘', ':^*': '😘', '>:P': '😜', 'X-P': '😜', '>:[': '😞', ':-(': '😞', ':-[': '😞', '>:(': '😠', ':\'(': '😢', ';-(': '😢', '>.<': '😣', '#-)': '😵', '%-)': '😵', 'X-)': '😵', '\\0/': '🙆', '\\O/': '🙆', '0:3': '😇', '0:)': '😇', 'O:)': '😇', 'O=)': '😇', 'O:3': '😇', 'B-)': '😎', '8-)': '😎', 'B-D': '😎', '8-D': '😎', '-_-': '😑', '>:\\': '😕', '>:/': '😕', ':-/': '😕', ':-.': '😕', ':-P': '😛', ':Þ': '😛', ':-b': '😛', ':-O': '😮', O_O: '😮', '>:O': '😮', ':-X': '😶', ':-#': '😶', ':-)': '🙂', '(y)': 'ðŸ‘', '<3': 'â¤', '=D': '😃', ';)': '😉', '*)': '😉', ';]': '😉', ';D': '😉', ':*': '😘', '=*': '😘', ':(': '😞', ':[': '😞', '=(': '😞', ':@': '😠', ';(': '😢', 'D:': '😨', ':$': '😳', '=$': '😳', '#)': '😵', '%)': '😵', 'X)': '😵', 'B)': '😎', '8)': '😎', ':/': '😕', ':\\': '😕', '=/': '😕', '=\\': '😕', ':L': '😕', '=L': '😕', ':P': '😛', '=P': '😛', ':b': '😛', ':O': '😮', ':X': '😶', ':#': '😶', '=X': '😶', '=#': '😶', ':)': '🙂', '=]': '🙂', '=)': '🙂', ':]': '🙂', ':D': '😄' }; +const ascii = { + '*\\0/*': '🙆', + '*\\O/*': '🙆', + '-___-': '😑', + ":'-)": '😂', + "':-)": '😅', + "':-D": '😅', + '>:-)': '😆', + "':-(": '😓', + '>:-(': '😠', + ":'-(": '😢', + 'O:-)': '😇', + '0:-3': '😇', + '0:-)': '😇', + '0;^)': '😇', + 'O;-)': '😇', + '0;-)': '😇', + 'O:-3': '😇', + '-__-': '😑', + ':-Þ': '😛', + '</3': '💔', + ":')": '😂', + ':-D': '😃', + "':)": '😅', + "'=)": '😅', + "':D": '😅', + "'=D": '😅', + '>:)': '😆', + '>;)': '😆', + '>=)': '😆', + ';-)': '😉', + '*-)': '😉', + ';-]': '😉', + ';^)': '😉', + "':(": '😓', + "'=(": '😓', + ':-*': '😘', + ':^*': '😘', + '>:P': '😜', + 'X-P': '😜', + '>:[': '😞', + ':-(': '😞', + ':-[': '😞', + '>:(': '😠', + ":'(": '😢', + ';-(': '😢', + '>.<': '😣', + '#-)': '😵', + '%-)': '😵', + 'X-)': '😵', + '\\0/': '🙆', + '\\O/': '🙆', + '0:3': '😇', + '0:)': '😇', + 'O:)': '😇', + 'O=)': '😇', + 'O:3': '😇', + 'B-)': '😎', + '8-)': '😎', + 'B-D': '😎', + '8-D': '😎', + '-_-': '😑', + '>:\\': '😕', + '>:/': '😕', + ':-/': '😕', + ':-.': '😕', + ':-P': '😛', + ':Þ': '😛', + ':-b': '😛', + ':-O': '😮', + 'O_O': '😮', + '>:O': '😮', + ':-X': '😶', + ':-#': '😶', + ':-)': '🙂', + '(y)': 'ðŸ‘', + '<3': 'â¤', + '=D': '😃', + ';)': '😉', + '*)': '😉', + ';]': '😉', + ';D': '😉', + ':*': '😘', + '=*': '😘', + ':(': '😞', + ':[': '😞', + '=(': '😞', + ':@': '😠', + ';(': '😢', + 'D:': '😨', + ':$': '😳', + '=$': '😳', + '#)': '😵', + '%)': '😵', + 'X)': '😵', + 'B)': '😎', + '8)': '😎', + ':/': '😕', + ':\\': '😕', + '=/': '😕', + '=\\': '😕', + ':L': '😕', + '=L': '😕', + ':P': '😛', + '=P': '😛', + ':b': '😛', + ':O': '😮', + ':X': '😶', + ':#': '😶', + '=X': '😶', + '=#': '😶', + ':)': '🙂', + '=]': '🙂', + '=)': '🙂', + ':]': '🙂', + ':D': '😄', +}; -export const asciiRegexp = '(\\*\\\\0\\/\\*|\\*\\\\O\\/\\*|\\-___\\-|\\:\'\\-\\)|\'\\:\\-\\)|\'\\:\\-D|\\>\\:\\-\\)|>\\:\\-\\)|\'\\:\\-\\(|\\>\\:\\-\\(|>\\:\\-\\(|\\:\'\\-\\(|O\\:\\-\\)|0\\:\\-3|0\\:\\-\\)|0;\\^\\)|O;\\-\\)|0;\\-\\)|O\\:\\-3|\\-__\\-|\\:\\-Þ|\\:\\-Þ|\\<\\/3|<\\/3|\\:\'\\)|\\:\\-D|\'\\:\\)|\'\\=\\)|\'\\:D|\'\\=D|\\>\\:\\)|>\\:\\)|\\>;\\)|>;\\)|\\>\\=\\)|>\\=\\)|;\\-\\)|\\*\\-\\)|;\\-\\]|;\\^\\)|\'\\:\\(|\'\\=\\(|\\:\\-\\*|\\:\\^\\*|\\>\\:P|>\\:P|X\\-P|\\>\\:\\[|>\\:\\[|\\:\\-\\(|\\:\\-\\[|\\>\\:\\(|>\\:\\(|\\:\'\\(|;\\-\\(|\\>\\.\\<|>\\.<|#\\-\\)|%\\-\\)|X\\-\\)|\\\\0\\/|\\\\O\\/|0\\:3|0\\:\\)|O\\:\\)|O\\=\\)|O\\:3|B\\-\\)|8\\-\\)|B\\-D|8\\-D|\\-_\\-|\\>\\:\\\\|>\\:\\\\|\\>\\:\\/|>\\:\\/|\\:\\-\\/|\\:\\-\\.|\\:\\-P|\\:Þ|\\:Þ|\\:\\-b|\\:\\-O|O_O|\\>\\:O|>\\:O|\\:\\-X|\\:\\-#|\\:\\-\\)|\\(y\\)|\\<3|<3|\\=D|;\\)|\\*\\)|;\\]|;D|\\:\\*|\\=\\*|\\:\\(|\\:\\[|\\=\\(|\\:@|;\\(|D\\:|\\:\\$|\\=\\$|#\\)|%\\)|X\\)|B\\)|8\\)|\\:\\/|\\:\\\\|\\=\\/|\\=\\\\|\\:L|\\=L|\\:P|\\=P|\\:b|\\:O|\\:X|\\:#|\\=X|\\=#|\\:\\)|\\=\\]|\\=\\)|\\:\\]|\\:D)'; +export const asciiRegexp = + "(\\*\\\\0\\/\\*|\\*\\\\O\\/\\*|\\-___\\-|\\:'\\-\\)|'\\:\\-\\)|'\\:\\-D|\\>\\:\\-\\)|>\\:\\-\\)|'\\:\\-\\(|\\>\\:\\-\\(|>\\:\\-\\(|\\:'\\-\\(|O\\:\\-\\)|0\\:\\-3|0\\:\\-\\)|0;\\^\\)|O;\\-\\)|0;\\-\\)|O\\:\\-3|\\-__\\-|\\:\\-Þ|\\:\\-Þ|\\<\\/3|<\\/3|\\:'\\)|\\:\\-D|'\\:\\)|'\\=\\)|'\\:D|'\\=D|\\>\\:\\)|>\\:\\)|\\>;\\)|>;\\)|\\>\\=\\)|>\\=\\)|;\\-\\)|\\*\\-\\)|;\\-\\]|;\\^\\)|'\\:\\(|'\\=\\(|\\:\\-\\*|\\:\\^\\*|\\>\\:P|>\\:P|X\\-P|\\>\\:\\[|>\\:\\[|\\:\\-\\(|\\:\\-\\[|\\>\\:\\(|>\\:\\(|\\:'\\(|;\\-\\(|\\>\\.\\<|>\\.<|#\\-\\)|%\\-\\)|X\\-\\)|\\\\0\\/|\\\\O\\/|0\\:3|0\\:\\)|O\\:\\)|O\\=\\)|O\\:3|B\\-\\)|8\\-\\)|B\\-D|8\\-D|\\-_\\-|\\>\\:\\\\|>\\:\\\\|\\>\\:\\/|>\\:\\/|\\:\\-\\/|\\:\\-\\.|\\:\\-P|\\:Þ|\\:Þ|\\:\\-b|\\:\\-O|O_O|\\>\\:O|>\\:O|\\:\\-X|\\:\\-#|\\:\\-\\)|\\(y\\)|\\<3|<3|\\=D|;\\)|\\*\\)|;\\]|;D|\\:\\*|\\=\\*|\\:\\(|\\:\\[|\\=\\(|\\:@|;\\(|D\\:|\\:\\$|\\=\\$|#\\)|%\\)|X\\)|B\\)|8\\)|\\:\\/|\\:\\\\|\\=\\/|\\=\\\\|\\:L|\\=L|\\:P|\\=P|\\:b|\\:O|\\:X|\\:#|\\=X|\\=#|\\:\\)|\\=\\]|\\=\\)|\\:\\]|\\:D)"; export default ascii; diff --git a/packages/livechat/src/components/Emoji/emojis.js b/packages/livechat/src/components/Emoji/emojis.js index 47f07d9df49..82083ef708c 100644 --- a/packages/livechat/src/components/Emoji/emojis.js +++ b/packages/livechat/src/components/Emoji/emojis.js @@ -1,2 +1,4630 @@ -const emojis = { ':england:': 'ðŸ´ó §ó ¢ó ¥ó ®ó §ó ¿', ':scotland:': 'ðŸ´ó §ó ¢ó ³ó £ó ´ó ¿', ':wales:': 'ðŸ´ó §ó ¢ó ·ó ¬ó ³ó ¿', ':men_holding_hands_medium_light_skin_tone_light_skin_tone:': '👨ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_tone2_tone1:': '👨ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_medium_skin_tone_light_skin_tone:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_tone3_tone1:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_tone3_tone2:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_tone4_tone1:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_tone4_tone2:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', ':men_holding_hands_tone4_tone3:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', ':men_holding_hands_dark_skin_tone_light_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_tone5_tone1:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', ':men_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_tone5_tone2:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':men_holding_hands_dark_skin_tone_medium_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', ':men_holding_hands_tone5_tone3:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', ':men_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':men_holding_hands_tone5_tone4:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':people_holding_hands_light_skin_tone:': '🧑ðŸ»â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_tone1:': '🧑ðŸ»â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_medium_light_skin_tone:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_tone2:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_medium_light_skin_tone_light_skin_tone:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_tone2_tone1:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_medium_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_tone3:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_medium_skin_tone_light_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_tone3_tone1:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_medium_skin_tone_medium_light_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_tone3_tone2:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_medium_dark_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¾', ':people_holding_hands_tone4:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¾', ':people_holding_hands_medium_dark_skin_tone_light_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_tone4_tone1:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_tone4_tone2:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_tone4_tone3:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_dark_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¿', ':people_holding_hands_tone5:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¿', ':people_holding_hands_dark_skin_tone_light_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_tone5_tone1:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ»', ':people_holding_hands_dark_skin_tone_medium_light_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_tone5_tone2:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¼', ':people_holding_hands_dark_skin_tone_medium_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_tone5_tone3:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ½', ':people_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¾', ':people_holding_hands_tone5_tone4:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¾', ':woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_tone1_tone2:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_light_skin_tone_medium_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_tone1_tone3:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_tone1_tone4:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_light_skin_tone_dark_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_tone1_tone5:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_tone2_tone1:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_tone2_tone3:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_tone2_tone4:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_tone2_tone5:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_medium_skin_tone_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_tone3_tone1:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_tone3_tone2:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_tone3_tone4:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_tone3_tone5:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_tone4_tone1:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_tone4_tone2:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_tone4_tone3:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_tone4_tone5:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¿', ':woman_and_man_holding_hands_dark_skin_tone_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_tone5_tone1:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', ':woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_tone5_tone2:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', ':woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_tone5_tone3:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', ':woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':woman_and_man_holding_hands_tone5_tone4:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', ':women_holding_hands_medium_light_skin_tone_light_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_tone2_tone1:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_medium_skin_tone_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_tone3_tone1:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_tone3_tone2:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_tone4_tone1:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_tone4_tone2:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ½', ':women_holding_hands_tone4_tone3:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ½', ':women_holding_hands_dark_skin_tone_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_tone5_tone1:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ»', ':women_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_tone5_tone2:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¼', ':women_holding_hands_dark_skin_tone_medium_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ½', ':women_holding_hands_tone5_tone3:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ½', ':women_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¾', ':women_holding_hands_tone5_tone4:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¾', ':family_mmbb:': '👨â€ðŸ‘¨â€ðŸ‘¦â€ðŸ‘¦', ':family_mmgb:': '👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘¦', ':family_mmgg:': '👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘§', ':family_mwbb:': '👨â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦', ':family_mwgb:': '👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦', ':family_mwgg:': '👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§', ':family_wwbb:': '👩â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦', ':family_wwgb:': '👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦', ':family_wwgg:': '👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§', ':couplekiss_mm:': '👨â€â¤ï¸â€ðŸ’‹ðŸ‘¨', ':kiss_mm:': '👨â€â¤ï¸â€ðŸ’‹ðŸ‘¨', ':kiss_woman_man:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘¨', ':couplekiss_ww:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘©', ':kiss_ww:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘©', ':family_man_boy_boy:': '👨â€ðŸ‘¦â€ðŸ‘¦', ':family_man_girl_boy:': '👨â€ðŸ‘§â€ðŸ‘¦', ':family_man_girl_girl:': '👨â€ðŸ‘§â€ðŸ‘§', ':family_man_woman_boy:': '👨â€ðŸ‘©â€ðŸ‘¦', ':family_mmb:': '👨â€ðŸ‘¨â€ðŸ‘¦', ':family_mmg:': '👨â€ðŸ‘¨â€ðŸ‘§', ':family_mwg:': '👨â€ðŸ‘©â€ðŸ‘§', ':family_woman_boy_boy:': '👩â€ðŸ‘¦â€ðŸ‘¦', ':family_woman_girl_boy:': '👩â€ðŸ‘§â€ðŸ‘¦', ':family_woman_girl_girl:': '👩â€ðŸ‘§â€ðŸ‘§', ':family_wwb:': '👩â€ðŸ‘©â€ðŸ‘¦', ':family_wwg:': '👩â€ðŸ‘©â€ðŸ‘§', ':man_artist_light_skin_tone:': '👨ðŸ»â€ðŸŽ¨', ':man_artist_tone1:': '👨ðŸ»â€ðŸŽ¨', ':man_artist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ¨', ':man_artist_tone2:': '👨ðŸ¼â€ðŸŽ¨', ':man_artist_medium_skin_tone:': '👨ðŸ½â€ðŸŽ¨', ':man_artist_tone3:': '👨ðŸ½â€ðŸŽ¨', ':man_artist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ¨', ':man_artist_tone4:': '👨ðŸ¾â€ðŸŽ¨', ':man_artist_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ¨', ':man_artist_tone5:': '👨ðŸ¿â€ðŸŽ¨', ':man_astronaut_light_skin_tone:': '👨ðŸ»â€ðŸš€', ':man_astronaut_tone1:': '👨ðŸ»â€ðŸš€', ':man_astronaut_medium_light_skin_tone:': '👨ðŸ¼â€ðŸš€', ':man_astronaut_tone2:': '👨ðŸ¼â€ðŸš€', ':man_astronaut_medium_skin_tone:': '👨ðŸ½â€ðŸš€', ':man_astronaut_tone3:': '👨ðŸ½â€ðŸš€', ':man_astronaut_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸš€', ':man_astronaut_tone4:': '👨ðŸ¾â€ðŸš€', ':man_astronaut_dark_skin_tone:': '👨ðŸ¿â€ðŸš€', ':man_astronaut_tone5:': '👨ðŸ¿â€ðŸš€', ':man_bald_light_skin_tone:': '👨ðŸ»â€ðŸ¦²', ':man_bald_tone1:': '👨ðŸ»â€ðŸ¦²', ':man_bald_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦²', ':man_bald_tone2:': '👨ðŸ¼â€ðŸ¦²', ':man_bald_medium_skin_tone:': '👨ðŸ½â€ðŸ¦²', ':man_bald_tone3:': '👨ðŸ½â€ðŸ¦²', ':man_bald_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦²', ':man_bald_tone4:': '👨ðŸ¾â€ðŸ¦²', ':man_bald_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦²', ':man_bald_tone5:': '👨ðŸ¿â€ðŸ¦²', ':man_cook_light_skin_tone:': '👨ðŸ»â€ðŸ³', ':man_cook_tone1:': '👨ðŸ»â€ðŸ³', ':man_cook_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ³', ':man_cook_tone2:': '👨ðŸ¼â€ðŸ³', ':man_cook_medium_skin_tone:': '👨ðŸ½â€ðŸ³', ':man_cook_tone3:': '👨ðŸ½â€ðŸ³', ':man_cook_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ³', ':man_cook_tone4:': '👨ðŸ¾â€ðŸ³', ':man_cook_dark_skin_tone:': '👨ðŸ¿â€ðŸ³', ':man_cook_tone5:': '👨ðŸ¿â€ðŸ³', ':man_curly_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦±', ':man_curly_haired_tone1:': '👨ðŸ»â€ðŸ¦±', ':man_curly_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦±', ':man_curly_haired_tone2:': '👨ðŸ¼â€ðŸ¦±', ':man_curly_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦±', ':man_curly_haired_tone3:': '👨ðŸ½â€ðŸ¦±', ':man_curly_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦±', ':man_curly_haired_tone4:': '👨ðŸ¾â€ðŸ¦±', ':man_curly_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦±', ':man_curly_haired_tone5:': '👨ðŸ¿â€ðŸ¦±', ':man_factory_worker_light_skin_tone:': '👨ðŸ»â€ðŸ', ':man_factory_worker_tone1:': '👨ðŸ»â€ðŸ', ':man_factory_worker_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ', ':man_factory_worker_tone2:': '👨ðŸ¼â€ðŸ', ':man_factory_worker_medium_skin_tone:': '👨ðŸ½â€ðŸ', ':man_factory_worker_tone3:': '👨ðŸ½â€ðŸ', ':man_factory_worker_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ', ':man_factory_worker_tone4:': '👨ðŸ¾â€ðŸ', ':man_factory_worker_dark_skin_tone:': '👨ðŸ¿â€ðŸ', ':man_factory_worker_tone5:': '👨ðŸ¿â€ðŸ', ':man_farmer_light_skin_tone:': '👨ðŸ»â€ðŸŒ¾', ':man_farmer_tone1:': '👨ðŸ»â€ðŸŒ¾', ':man_farmer_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŒ¾', ':man_farmer_tone2:': '👨ðŸ¼â€ðŸŒ¾', ':man_farmer_medium_skin_tone:': '👨ðŸ½â€ðŸŒ¾', ':man_farmer_tone3:': '👨ðŸ½â€ðŸŒ¾', ':man_farmer_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŒ¾', ':man_farmer_tone4:': '👨ðŸ¾â€ðŸŒ¾', ':man_farmer_dark_skin_tone:': '👨ðŸ¿â€ðŸŒ¾', ':man_farmer_tone5:': '👨ðŸ¿â€ðŸŒ¾', ':man_firefighter_light_skin_tone:': '👨ðŸ»â€ðŸš’', ':man_firefighter_tone1:': '👨ðŸ»â€ðŸš’', ':man_firefighter_medium_light_skin_tone:': '👨ðŸ¼â€ðŸš’', ':man_firefighter_tone2:': '👨ðŸ¼â€ðŸš’', ':man_firefighter_medium_skin_tone:': '👨ðŸ½â€ðŸš’', ':man_firefighter_tone3:': '👨ðŸ½â€ðŸš’', ':man_firefighter_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸš’', ':man_firefighter_tone4:': '👨ðŸ¾â€ðŸš’', ':man_firefighter_dark_skin_tone:': '👨ðŸ¿â€ðŸš’', ':man_firefighter_tone5:': '👨ðŸ¿â€ðŸš’', ':man_in_manual_wheelchair_light_skin_tone:': '👨ðŸ»â€ðŸ¦½', ':man_in_manual_wheelchair_tone1:': '👨ðŸ»â€ðŸ¦½', ':man_in_manual_wheelchair_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦½', ':man_in_manual_wheelchair_tone2:': '👨ðŸ¼â€ðŸ¦½', ':man_in_manual_wheelchair_medium_skin_tone:': '👨ðŸ½â€ðŸ¦½', ':man_in_manual_wheelchair_tone3:': '👨ðŸ½â€ðŸ¦½', ':man_in_manual_wheelchair_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦½', ':man_in_manual_wheelchair_tone4:': '👨ðŸ¾â€ðŸ¦½', ':man_in_manual_wheelchair_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦½', ':man_in_manual_wheelchair_tone5:': '👨ðŸ¿â€ðŸ¦½', ':man_in_motorized_wheelchair_light_skin_tone:': '👨ðŸ»â€ðŸ¦¼', ':man_in_motorized_wheelchair_tone1:': '👨ðŸ»â€ðŸ¦¼', ':man_in_motorized_wheelchair_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦¼', ':man_in_motorized_wheelchair_tone2:': '👨ðŸ¼â€ðŸ¦¼', ':man_in_motorized_wheelchair_medium_skin_tone:': '👨ðŸ½â€ðŸ¦¼', ':man_in_motorized_wheelchair_tone3:': '👨ðŸ½â€ðŸ¦¼', ':man_in_motorized_wheelchair_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦¼', ':man_in_motorized_wheelchair_tone4:': '👨ðŸ¾â€ðŸ¦¼', ':man_in_motorized_wheelchair_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦¼', ':man_in_motorized_wheelchair_tone5:': '👨ðŸ¿â€ðŸ¦¼', ':man_mechanic_light_skin_tone:': '👨ðŸ»â€ðŸ”§', ':man_mechanic_tone1:': '👨ðŸ»â€ðŸ”§', ':man_mechanic_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ”§', ':man_mechanic_tone2:': '👨ðŸ¼â€ðŸ”§', ':man_mechanic_medium_skin_tone:': '👨ðŸ½â€ðŸ”§', ':man_mechanic_tone3:': '👨ðŸ½â€ðŸ”§', ':man_mechanic_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ”§', ':man_mechanic_tone4:': '👨ðŸ¾â€ðŸ”§', ':man_mechanic_dark_skin_tone:': '👨ðŸ¿â€ðŸ”§', ':man_mechanic_tone5:': '👨ðŸ¿â€ðŸ”§', ':man_office_worker_light_skin_tone:': '👨ðŸ»â€ðŸ’¼', ':man_office_worker_tone1:': '👨ðŸ»â€ðŸ’¼', ':man_office_worker_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ’¼', ':man_office_worker_tone2:': '👨ðŸ¼â€ðŸ’¼', ':man_office_worker_medium_skin_tone:': '👨ðŸ½â€ðŸ’¼', ':man_office_worker_tone3:': '👨ðŸ½â€ðŸ’¼', ':man_office_worker_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ’¼', ':man_office_worker_tone4:': '👨ðŸ¾â€ðŸ’¼', ':man_office_worker_dark_skin_tone:': '👨ðŸ¿â€ðŸ’¼', ':man_office_worker_tone5:': '👨ðŸ¿â€ðŸ’¼', ':man_red_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦°', ':man_red_haired_tone1:': '👨ðŸ»â€ðŸ¦°', ':man_red_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦°', ':man_red_haired_tone2:': '👨ðŸ¼â€ðŸ¦°', ':man_red_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦°', ':man_red_haired_tone3:': '👨ðŸ½â€ðŸ¦°', ':man_red_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦°', ':man_red_haired_tone4:': '👨ðŸ¾â€ðŸ¦°', ':man_red_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦°', ':man_red_haired_tone5:': '👨ðŸ¿â€ðŸ¦°', ':man_scientist_light_skin_tone:': '👨ðŸ»â€ðŸ”¬', ':man_scientist_tone1:': '👨ðŸ»â€ðŸ”¬', ':man_scientist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ”¬', ':man_scientist_tone2:': '👨ðŸ¼â€ðŸ”¬', ':man_scientist_medium_skin_tone:': '👨ðŸ½â€ðŸ”¬', ':man_scientist_tone3:': '👨ðŸ½â€ðŸ”¬', ':man_scientist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ”¬', ':man_scientist_tone4:': '👨ðŸ¾â€ðŸ”¬', ':man_scientist_dark_skin_tone:': '👨ðŸ¿â€ðŸ”¬', ':man_scientist_tone5:': '👨ðŸ¿â€ðŸ”¬', ':man_singer_light_skin_tone:': '👨ðŸ»â€ðŸŽ¤', ':man_singer_tone1:': '👨ðŸ»â€ðŸŽ¤', ':man_singer_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ¤', ':man_singer_tone2:': '👨ðŸ¼â€ðŸŽ¤', ':man_singer_medium_skin_tone:': '👨ðŸ½â€ðŸŽ¤', ':man_singer_tone3:': '👨ðŸ½â€ðŸŽ¤', ':man_singer_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ¤', ':man_singer_tone4:': '👨ðŸ¾â€ðŸŽ¤', ':man_singer_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ¤', ':man_singer_tone5:': '👨ðŸ¿â€ðŸŽ¤', ':man_student_light_skin_tone:': '👨ðŸ»â€ðŸŽ“', ':man_student_tone1:': '👨ðŸ»â€ðŸŽ“', ':man_student_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ“', ':man_student_tone2:': '👨ðŸ¼â€ðŸŽ“', ':man_student_medium_skin_tone:': '👨ðŸ½â€ðŸŽ“', ':man_student_tone3:': '👨ðŸ½â€ðŸŽ“', ':man_student_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ“', ':man_student_tone4:': '👨ðŸ¾â€ðŸŽ“', ':man_student_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ“', ':man_student_tone5:': '👨ðŸ¿â€ðŸŽ“', ':man_teacher_light_skin_tone:': '👨ðŸ»â€ðŸ«', ':man_teacher_tone1:': '👨ðŸ»â€ðŸ«', ':man_teacher_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ«', ':man_teacher_tone2:': '👨ðŸ¼â€ðŸ«', ':man_teacher_medium_skin_tone:': '👨ðŸ½â€ðŸ«', ':man_teacher_tone3:': '👨ðŸ½â€ðŸ«', ':man_teacher_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ«', ':man_teacher_tone4:': '👨ðŸ¾â€ðŸ«', ':man_teacher_dark_skin_tone:': '👨ðŸ¿â€ðŸ«', ':man_teacher_tone5:': '👨ðŸ¿â€ðŸ«', ':man_technologist_light_skin_tone:': '👨ðŸ»â€ðŸ’»', ':man_technologist_tone1:': '👨ðŸ»â€ðŸ’»', ':man_technologist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ’»', ':man_technologist_tone2:': '👨ðŸ¼â€ðŸ’»', ':man_technologist_medium_skin_tone:': '👨ðŸ½â€ðŸ’»', ':man_technologist_tone3:': '👨ðŸ½â€ðŸ’»', ':man_technologist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ’»', ':man_technologist_tone4:': '👨ðŸ¾â€ðŸ’»', ':man_technologist_dark_skin_tone:': '👨ðŸ¿â€ðŸ’»', ':man_technologist_tone5:': '👨ðŸ¿â€ðŸ’»', ':man_white_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦³', ':man_white_haired_tone1:': '👨ðŸ»â€ðŸ¦³', ':man_white_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦³', ':man_white_haired_tone2:': '👨ðŸ¼â€ðŸ¦³', ':man_white_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦³', ':man_white_haired_tone3:': '👨ðŸ½â€ðŸ¦³', ':man_white_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦³', ':man_white_haired_tone4:': '👨ðŸ¾â€ðŸ¦³', ':man_white_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦³', ':man_white_haired_tone5:': '👨ðŸ¿â€ðŸ¦³', ':man_with_probing_cane_light_skin_tone:': '👨ðŸ»â€ðŸ¦¯', ':man_with_probing_cane_tone1:': '👨ðŸ»â€ðŸ¦¯', ':man_with_probing_cane_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦¯', ':man_with_probing_cane_tone2:': '👨ðŸ¼â€ðŸ¦¯', ':man_with_probing_cane_medium_skin_tone:': '👨ðŸ½â€ðŸ¦¯', ':man_with_probing_cane_tone3:': '👨ðŸ½â€ðŸ¦¯', ':man_with_probing_cane_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦¯', ':man_with_probing_cane_tone4:': '👨ðŸ¾â€ðŸ¦¯', ':man_with_probing_cane_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦¯', ':man_with_probing_cane_tone5:': '👨ðŸ¿â€ðŸ¦¯', ':people_holding_hands:': '🧑â€ðŸ¤â€ðŸ§‘', ':woman_artist_light_skin_tone:': '👩ðŸ»â€ðŸŽ¨', ':woman_artist_tone1:': '👩ðŸ»â€ðŸŽ¨', ':woman_artist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ¨', ':woman_artist_tone2:': '👩ðŸ¼â€ðŸŽ¨', ':woman_artist_medium_skin_tone:': '👩ðŸ½â€ðŸŽ¨', ':woman_artist_tone3:': '👩ðŸ½â€ðŸŽ¨', ':woman_artist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ¨', ':woman_artist_tone4:': '👩ðŸ¾â€ðŸŽ¨', ':woman_artist_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ¨', ':woman_artist_tone5:': '👩ðŸ¿â€ðŸŽ¨', ':woman_astronaut_light_skin_tone:': '👩ðŸ»â€ðŸš€', ':woman_astronaut_tone1:': '👩ðŸ»â€ðŸš€', ':woman_astronaut_medium_light_skin_tone:': '👩ðŸ¼â€ðŸš€', ':woman_astronaut_tone2:': '👩ðŸ¼â€ðŸš€', ':woman_astronaut_medium_skin_tone:': '👩ðŸ½â€ðŸš€', ':woman_astronaut_tone3:': '👩ðŸ½â€ðŸš€', ':woman_astronaut_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸš€', ':woman_astronaut_tone4:': '👩ðŸ¾â€ðŸš€', ':woman_astronaut_dark_skin_tone:': '👩ðŸ¿â€ðŸš€', ':woman_astronaut_tone5:': '👩ðŸ¿â€ðŸš€', ':woman_bald_light_skin_tone:': '👩ðŸ»â€ðŸ¦²', ':woman_bald_tone1:': '👩ðŸ»â€ðŸ¦²', ':woman_bald_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦²', ':woman_bald_tone2:': '👩ðŸ¼â€ðŸ¦²', ':woman_bald_medium_skin_tone:': '👩ðŸ½â€ðŸ¦²', ':woman_bald_tone3:': '👩ðŸ½â€ðŸ¦²', ':woman_bald_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦²', ':woman_bald_tone4:': '👩ðŸ¾â€ðŸ¦²', ':woman_bald_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦²', ':woman_bald_tone5:': '👩ðŸ¿â€ðŸ¦²', ':woman_cook_light_skin_tone:': '👩ðŸ»â€ðŸ³', ':woman_cook_tone1:': '👩ðŸ»â€ðŸ³', ':woman_cook_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ³', ':woman_cook_tone2:': '👩ðŸ¼â€ðŸ³', ':woman_cook_medium_skin_tone:': '👩ðŸ½â€ðŸ³', ':woman_cook_tone3:': '👩ðŸ½â€ðŸ³', ':woman_cook_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ³', ':woman_cook_tone4:': '👩ðŸ¾â€ðŸ³', ':woman_cook_dark_skin_tone:': '👩ðŸ¿â€ðŸ³', ':woman_cook_tone5:': '👩ðŸ¿â€ðŸ³', ':woman_curly_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦±', ':woman_curly_haired_tone1:': '👩ðŸ»â€ðŸ¦±', ':woman_curly_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦±', ':woman_curly_haired_tone2:': '👩ðŸ¼â€ðŸ¦±', ':woman_curly_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦±', ':woman_curly_haired_tone3:': '👩ðŸ½â€ðŸ¦±', ':woman_curly_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦±', ':woman_curly_haired_tone4:': '👩ðŸ¾â€ðŸ¦±', ':woman_curly_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦±', ':woman_curly_haired_tone5:': '👩ðŸ¿â€ðŸ¦±', ':woman_factory_worker_light_skin_tone:': '👩ðŸ»â€ðŸ', ':woman_factory_worker_tone1:': '👩ðŸ»â€ðŸ', ':woman_factory_worker_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ', ':woman_factory_worker_tone2:': '👩ðŸ¼â€ðŸ', ':woman_factory_worker_medium_skin_tone:': '👩ðŸ½â€ðŸ', ':woman_factory_worker_tone3:': '👩ðŸ½â€ðŸ', ':woman_factory_worker_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ', ':woman_factory_worker_tone4:': '👩ðŸ¾â€ðŸ', ':woman_factory_worker_dark_skin_tone:': '👩ðŸ¿â€ðŸ', ':woman_factory_worker_tone5:': '👩ðŸ¿â€ðŸ', ':woman_farmer_light_skin_tone:': '👩ðŸ»â€ðŸŒ¾', ':woman_farmer_tone1:': '👩ðŸ»â€ðŸŒ¾', ':woman_farmer_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŒ¾', ':woman_farmer_tone2:': '👩ðŸ¼â€ðŸŒ¾', ':woman_farmer_medium_skin_tone:': '👩ðŸ½â€ðŸŒ¾', ':woman_farmer_tone3:': '👩ðŸ½â€ðŸŒ¾', ':woman_farmer_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŒ¾', ':woman_farmer_tone4:': '👩ðŸ¾â€ðŸŒ¾', ':woman_farmer_dark_skin_tone:': '👩ðŸ¿â€ðŸŒ¾', ':woman_farmer_tone5:': '👩ðŸ¿â€ðŸŒ¾', ':woman_firefighter_light_skin_tone:': '👩ðŸ»â€ðŸš’', ':woman_firefighter_tone1:': '👩ðŸ»â€ðŸš’', ':woman_firefighter_medium_light_skin_tone:': '👩ðŸ¼â€ðŸš’', ':woman_firefighter_tone2:': '👩ðŸ¼â€ðŸš’', ':woman_firefighter_medium_skin_tone:': '👩ðŸ½â€ðŸš’', ':woman_firefighter_tone3:': '👩ðŸ½â€ðŸš’', ':woman_firefighter_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸš’', ':woman_firefighter_tone4:': '👩ðŸ¾â€ðŸš’', ':woman_firefighter_dark_skin_tone:': '👩ðŸ¿â€ðŸš’', ':woman_firefighter_tone5:': '👩ðŸ¿â€ðŸš’', ':woman_in_manual_wheelchair_light_skin_tone:': '👩ðŸ»â€ðŸ¦½', ':woman_in_manual_wheelchair_tone1:': '👩ðŸ»â€ðŸ¦½', ':woman_in_manual_wheelchair_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦½', ':woman_in_manual_wheelchair_tone2:': '👩ðŸ¼â€ðŸ¦½', ':woman_in_manual_wheelchair_medium_skin_tone:': '👩ðŸ½â€ðŸ¦½', ':woman_in_manual_wheelchair_tone3:': '👩ðŸ½â€ðŸ¦½', ':woman_in_manual_wheelchair_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦½', ':woman_in_manual_wheelchair_tone4:': '👩ðŸ¾â€ðŸ¦½', ':woman_in_manual_wheelchair_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦½', ':woman_in_manual_wheelchair_tone5:': '👩ðŸ¿â€ðŸ¦½', ':woman_in_motorized_wheelchair_light_skin_tone:': '👩ðŸ»â€ðŸ¦¼', ':woman_in_motorized_wheelchair_tone1:': '👩ðŸ»â€ðŸ¦¼', ':woman_in_motorized_wheelchair_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦¼', ':woman_in_motorized_wheelchair_tone2:': '👩ðŸ¼â€ðŸ¦¼', ':woman_in_motorized_wheelchair_medium_skin_tone:': '👩ðŸ½â€ðŸ¦¼', ':woman_in_motorized_wheelchair_tone3:': '👩ðŸ½â€ðŸ¦¼', ':woman_in_motorized_wheelchair_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦¼', ':woman_in_motorized_wheelchair_tone4:': '👩ðŸ¾â€ðŸ¦¼', ':woman_in_motorized_wheelchair_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦¼', ':woman_in_motorized_wheelchair_tone5:': '👩ðŸ¿â€ðŸ¦¼', ':woman_mechanic_light_skin_tone:': '👩ðŸ»â€ðŸ”§', ':woman_mechanic_tone1:': '👩ðŸ»â€ðŸ”§', ':woman_mechanic_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ”§', ':woman_mechanic_tone2:': '👩ðŸ¼â€ðŸ”§', ':woman_mechanic_medium_skin_tone:': '👩ðŸ½â€ðŸ”§', ':woman_mechanic_tone3:': '👩ðŸ½â€ðŸ”§', ':woman_mechanic_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ”§', ':woman_mechanic_tone4:': '👩ðŸ¾â€ðŸ”§', ':woman_mechanic_dark_skin_tone:': '👩ðŸ¿â€ðŸ”§', ':woman_mechanic_tone5:': '👩ðŸ¿â€ðŸ”§', ':woman_office_worker_light_skin_tone:': '👩ðŸ»â€ðŸ’¼', ':woman_office_worker_tone1:': '👩ðŸ»â€ðŸ’¼', ':woman_office_worker_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ’¼', ':woman_office_worker_tone2:': '👩ðŸ¼â€ðŸ’¼', ':woman_office_worker_medium_skin_tone:': '👩ðŸ½â€ðŸ’¼', ':woman_office_worker_tone3:': '👩ðŸ½â€ðŸ’¼', ':woman_office_worker_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ’¼', ':woman_office_worker_tone4:': '👩ðŸ¾â€ðŸ’¼', ':woman_office_worker_dark_skin_tone:': '👩ðŸ¿â€ðŸ’¼', ':woman_office_worker_tone5:': '👩ðŸ¿â€ðŸ’¼', ':woman_red_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦°', ':woman_red_haired_tone1:': '👩ðŸ»â€ðŸ¦°', ':woman_red_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦°', ':woman_red_haired_tone2:': '👩ðŸ¼â€ðŸ¦°', ':woman_red_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦°', ':woman_red_haired_tone3:': '👩ðŸ½â€ðŸ¦°', ':woman_red_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦°', ':woman_red_haired_tone4:': '👩ðŸ¾â€ðŸ¦°', ':woman_red_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦°', ':woman_red_haired_tone5:': '👩ðŸ¿â€ðŸ¦°', ':woman_scientist_light_skin_tone:': '👩ðŸ»â€ðŸ”¬', ':woman_scientist_tone1:': '👩ðŸ»â€ðŸ”¬', ':woman_scientist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ”¬', ':woman_scientist_tone2:': '👩ðŸ¼â€ðŸ”¬', ':woman_scientist_medium_skin_tone:': '👩ðŸ½â€ðŸ”¬', ':woman_scientist_tone3:': '👩ðŸ½â€ðŸ”¬', ':woman_scientist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ”¬', ':woman_scientist_tone4:': '👩ðŸ¾â€ðŸ”¬', ':woman_scientist_dark_skin_tone:': '👩ðŸ¿â€ðŸ”¬', ':woman_scientist_tone5:': '👩ðŸ¿â€ðŸ”¬', ':woman_singer_light_skin_tone:': '👩ðŸ»â€ðŸŽ¤', ':woman_singer_tone1:': '👩ðŸ»â€ðŸŽ¤', ':woman_singer_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ¤', ':woman_singer_tone2:': '👩ðŸ¼â€ðŸŽ¤', ':woman_singer_medium_skin_tone:': '👩ðŸ½â€ðŸŽ¤', ':woman_singer_tone3:': '👩ðŸ½â€ðŸŽ¤', ':woman_singer_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ¤', ':woman_singer_tone4:': '👩ðŸ¾â€ðŸŽ¤', ':woman_singer_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ¤', ':woman_singer_tone5:': '👩ðŸ¿â€ðŸŽ¤', ':woman_student_light_skin_tone:': '👩ðŸ»â€ðŸŽ“', ':woman_student_tone1:': '👩ðŸ»â€ðŸŽ“', ':woman_student_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ“', ':woman_student_tone2:': '👩ðŸ¼â€ðŸŽ“', ':woman_student_medium_skin_tone:': '👩ðŸ½â€ðŸŽ“', ':woman_student_tone3:': '👩ðŸ½â€ðŸŽ“', ':woman_student_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ“', ':woman_student_tone4:': '👩ðŸ¾â€ðŸŽ“', ':woman_student_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ“', ':woman_student_tone5:': '👩ðŸ¿â€ðŸŽ“', ':woman_teacher_light_skin_tone:': '👩ðŸ»â€ðŸ«', ':woman_teacher_tone1:': '👩ðŸ»â€ðŸ«', ':woman_teacher_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ«', ':woman_teacher_tone2:': '👩ðŸ¼â€ðŸ«', ':woman_teacher_medium_skin_tone:': '👩ðŸ½â€ðŸ«', ':woman_teacher_tone3:': '👩ðŸ½â€ðŸ«', ':woman_teacher_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ«', ':woman_teacher_tone4:': '👩ðŸ¾â€ðŸ«', ':woman_teacher_dark_skin_tone:': '👩ðŸ¿â€ðŸ«', ':woman_teacher_tone5:': '👩ðŸ¿â€ðŸ«', ':woman_technologist_light_skin_tone:': '👩ðŸ»â€ðŸ’»', ':woman_technologist_tone1:': '👩ðŸ»â€ðŸ’»', ':woman_technologist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ’»', ':woman_technologist_tone2:': '👩ðŸ¼â€ðŸ’»', ':woman_technologist_medium_skin_tone:': '👩ðŸ½â€ðŸ’»', ':woman_technologist_tone3:': '👩ðŸ½â€ðŸ’»', ':woman_technologist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ’»', ':woman_technologist_tone4:': '👩ðŸ¾â€ðŸ’»', ':woman_technologist_dark_skin_tone:': '👩ðŸ¿â€ðŸ’»', ':woman_technologist_tone5:': '👩ðŸ¿â€ðŸ’»', ':woman_white_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦³', ':woman_white_haired_tone1:': '👩ðŸ»â€ðŸ¦³', ':woman_white_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦³', ':woman_white_haired_tone2:': '👩ðŸ¼â€ðŸ¦³', ':woman_white_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦³', ':woman_white_haired_tone3:': '👩ðŸ½â€ðŸ¦³', ':woman_white_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦³', ':woman_white_haired_tone4:': '👩ðŸ¾â€ðŸ¦³', ':woman_white_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦³', ':woman_white_haired_tone5:': '👩ðŸ¿â€ðŸ¦³', ':woman_with_probing_cane_light_skin_tone:': '👩ðŸ»â€ðŸ¦¯', ':woman_with_probing_cane_tone1:': '👩ðŸ»â€ðŸ¦¯', ':woman_with_probing_cane_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦¯', ':woman_with_probing_cane_tone2:': '👩ðŸ¼â€ðŸ¦¯', ':woman_with_probing_cane_medium_skin_tone:': '👩ðŸ½â€ðŸ¦¯', ':woman_with_probing_cane_tone3:': '👩ðŸ½â€ðŸ¦¯', ':woman_with_probing_cane_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦¯', ':woman_with_probing_cane_tone4:': '👩ðŸ¾â€ðŸ¦¯', ':woman_with_probing_cane_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦¯', ':woman_with_probing_cane_tone5:': '👩ðŸ¿â€ðŸ¦¯', ':blond-haired_man_light_skin_tone:': '👱ðŸ»â€â™‚ï¸', ':blond-haired_man_tone1:': '👱ðŸ»â€â™‚ï¸', ':blond-haired_man_medium_light_skin_tone:': '👱ðŸ¼â€â™‚ï¸', ':blond-haired_man_tone2:': '👱ðŸ¼â€â™‚ï¸', ':blond-haired_man_medium_skin_tone:': '👱ðŸ½â€â™‚ï¸', ':blond-haired_man_tone3:': '👱ðŸ½â€â™‚ï¸', ':blond-haired_man_medium_dark_skin_tone:': '👱ðŸ¾â€â™‚ï¸', ':blond-haired_man_tone4:': '👱ðŸ¾â€â™‚ï¸', ':blond-haired_man_dark_skin_tone:': '👱ðŸ¿â€â™‚ï¸', ':blond-haired_man_tone5:': '👱ðŸ¿â€â™‚ï¸', ':blond-haired_woman_light_skin_tone:': '👱ðŸ»â€â™€ï¸', ':blond-haired_woman_tone1:': '👱ðŸ»â€â™€ï¸', ':blond-haired_woman_medium_light_skin_tone:': '👱ðŸ¼â€â™€ï¸', ':blond-haired_woman_tone2:': '👱ðŸ¼â€â™€ï¸', ':blond-haired_woman_medium_skin_tone:': '👱ðŸ½â€â™€ï¸', ':blond-haired_woman_tone3:': '👱ðŸ½â€â™€ï¸', ':blond-haired_woman_medium_dark_skin_tone:': '👱ðŸ¾â€â™€ï¸', ':blond-haired_woman_tone4:': '👱ðŸ¾â€â™€ï¸', ':blond-haired_woman_dark_skin_tone:': '👱ðŸ¿â€â™€ï¸', ':blond-haired_woman_tone5:': '👱ðŸ¿â€â™€ï¸', ':couple_with_heart_mm:': '👨â€â¤ï¸â€ðŸ‘¨', ':couple_mm:': '👨â€â¤ï¸â€ðŸ‘¨', ':couple_with_heart_woman_man:': '👩â€â¤ï¸â€ðŸ‘¨', ':couple_with_heart_ww:': '👩â€â¤ï¸â€ðŸ‘©', ':couple_ww:': '👩â€â¤ï¸â€ðŸ‘©', ':deaf_man_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', ':deaf_man_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', ':deaf_man_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':deaf_man_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':deaf_man_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', ':deaf_man_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', ':deaf_man_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':deaf_man_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':deaf_man_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':deaf_man_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':deaf_woman_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', ':deaf_woman_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', ':deaf_woman_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', ':deaf_woman_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', ':deaf_woman_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', ':deaf_woman_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', ':deaf_woman_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', ':deaf_woman_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', ':deaf_woman_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', ':deaf_woman_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', ':man_biking_light_skin_tone:': '🚴ðŸ»â€â™‚ï¸', ':man_biking_tone1:': '🚴ðŸ»â€â™‚ï¸', ':man_biking_medium_light_skin_tone:': '🚴ðŸ¼â€â™‚ï¸', ':man_biking_tone2:': '🚴ðŸ¼â€â™‚ï¸', ':man_biking_medium_skin_tone:': '🚴ðŸ½â€â™‚ï¸', ':man_biking_tone3:': '🚴ðŸ½â€â™‚ï¸', ':man_biking_medium_dark_skin_tone:': '🚴ðŸ¾â€â™‚ï¸', ':man_biking_tone4:': '🚴ðŸ¾â€â™‚ï¸', ':man_biking_dark_skin_tone:': '🚴ðŸ¿â€â™‚ï¸', ':man_biking_tone5:': '🚴ðŸ¿â€â™‚ï¸', ':man_bowing_light_skin_tone:': '🙇ðŸ»â€â™‚ï¸', ':man_bowing_tone1:': '🙇ðŸ»â€â™‚ï¸', ':man_bowing_medium_light_skin_tone:': '🙇ðŸ¼â€â™‚ï¸', ':man_bowing_tone2:': '🙇ðŸ¼â€â™‚ï¸', ':man_bowing_medium_skin_tone:': '🙇ðŸ½â€â™‚ï¸', ':man_bowing_tone3:': '🙇ðŸ½â€â™‚ï¸', ':man_bowing_medium_dark_skin_tone:': '🙇ðŸ¾â€â™‚ï¸', ':man_bowing_tone4:': '🙇ðŸ¾â€â™‚ï¸', ':man_bowing_dark_skin_tone:': '🙇ðŸ¿â€â™‚ï¸', ':man_bowing_tone5:': '🙇ðŸ¿â€â™‚ï¸', ':man_cartwheeling_light_skin_tone:': '🤸ðŸ»â€â™‚ï¸', ':man_cartwheeling_tone1:': '🤸ðŸ»â€â™‚ï¸', ':man_cartwheeling_medium_light_skin_tone:': '🤸ðŸ¼â€â™‚ï¸', ':man_cartwheeling_tone2:': '🤸ðŸ¼â€â™‚ï¸', ':man_cartwheeling_medium_skin_tone:': '🤸ðŸ½â€â™‚ï¸', ':man_cartwheeling_tone3:': '🤸ðŸ½â€â™‚ï¸', ':man_cartwheeling_medium_dark_skin_tone:': '🤸ðŸ¾â€â™‚ï¸', ':man_cartwheeling_tone4:': '🤸ðŸ¾â€â™‚ï¸', ':man_cartwheeling_dark_skin_tone:': '🤸ðŸ¿â€â™‚ï¸', ':man_cartwheeling_tone5:': '🤸ðŸ¿â€â™‚ï¸', ':man_climbing_light_skin_tone:': '🧗ðŸ»â€â™‚ï¸', ':man_climbing_tone1:': '🧗ðŸ»â€â™‚ï¸', ':man_climbing_medium_light_skin_tone:': '🧗ðŸ¼â€â™‚ï¸', ':man_climbing_tone2:': '🧗ðŸ¼â€â™‚ï¸', ':man_climbing_medium_skin_tone:': '🧗ðŸ½â€â™‚ï¸', ':man_climbing_tone3:': '🧗ðŸ½â€â™‚ï¸', ':man_climbing_medium_dark_skin_tone:': '🧗ðŸ¾â€â™‚ï¸', ':man_climbing_tone4:': '🧗ðŸ¾â€â™‚ï¸', ':man_climbing_dark_skin_tone:': '🧗ðŸ¿â€â™‚ï¸', ':man_climbing_tone5:': '🧗ðŸ¿â€â™‚ï¸', ':man_construction_worker_light_skin_tone:': '👷ðŸ»â€â™‚ï¸', ':man_construction_worker_tone1:': '👷ðŸ»â€â™‚ï¸', ':man_construction_worker_medium_light_skin_tone:': '👷ðŸ¼â€â™‚ï¸', ':man_construction_worker_tone2:': '👷ðŸ¼â€â™‚ï¸', ':man_construction_worker_medium_skin_tone:': '👷ðŸ½â€â™‚ï¸', ':man_construction_worker_tone3:': '👷ðŸ½â€â™‚ï¸', ':man_construction_worker_medium_dark_skin_tone:': '👷ðŸ¾â€â™‚ï¸', ':man_construction_worker_tone4:': '👷ðŸ¾â€â™‚ï¸', ':man_construction_worker_dark_skin_tone:': '👷ðŸ¿â€â™‚ï¸', ':man_construction_worker_tone5:': '👷ðŸ¿â€â™‚ï¸', ':man_detective_light_skin_tone:': '🕵ï¸ðŸ»â€â™‚ï¸', ':man_detective_tone1:': '🕵ï¸ðŸ»â€â™‚ï¸', ':man_detective_medium_light_skin_tone:': '🕵ï¸ðŸ¼â€â™‚ï¸', ':man_detective_tone2:': '🕵ï¸ðŸ¼â€â™‚ï¸', ':man_detective_medium_skin_tone:': '🕵ï¸ðŸ½â€â™‚ï¸', ':man_detective_tone3:': '🕵ï¸ðŸ½â€â™‚ï¸', ':man_detective_medium_dark_skin_tone:': '🕵ï¸ðŸ¾â€â™‚ï¸', ':man_detective_tone4:': '🕵ï¸ðŸ¾â€â™‚ï¸', ':man_detective_dark_skin_tone:': '🕵ï¸ðŸ¿â€â™‚ï¸', ':man_detective_tone5:': '🕵ï¸ðŸ¿â€â™‚ï¸', ':man_elf_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', ':man_elf_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', ':man_elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':man_elf_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':man_elf_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', ':man_elf_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', ':man_elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':man_elf_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':man_elf_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':man_elf_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':man_facepalming_light_skin_tone:': '🤦ðŸ»â€â™‚ï¸', ':man_facepalming_tone1:': '🤦ðŸ»â€â™‚ï¸', ':man_facepalming_medium_light_skin_tone:': '🤦ðŸ¼â€â™‚ï¸', ':man_facepalming_tone2:': '🤦ðŸ¼â€â™‚ï¸', ':man_facepalming_medium_skin_tone:': '🤦ðŸ½â€â™‚ï¸', ':man_facepalming_tone3:': '🤦ðŸ½â€â™‚ï¸', ':man_facepalming_medium_dark_skin_tone:': '🤦ðŸ¾â€â™‚ï¸', ':man_facepalming_tone4:': '🤦ðŸ¾â€â™‚ï¸', ':man_facepalming_dark_skin_tone:': '🤦ðŸ¿â€â™‚ï¸', ':man_facepalming_tone5:': '🤦ðŸ¿â€â™‚ï¸', ':man_fairy_light_skin_tone:': '🧚ðŸ»â€â™‚ï¸', ':man_fairy_tone1:': '🧚ðŸ»â€â™‚ï¸', ':man_fairy_medium_light_skin_tone:': '🧚ðŸ¼â€â™‚ï¸', ':man_fairy_tone2:': '🧚ðŸ¼â€â™‚ï¸', ':man_fairy_medium_skin_tone:': '🧚ðŸ½â€â™‚ï¸', ':man_fairy_tone3:': '🧚ðŸ½â€â™‚ï¸', ':man_fairy_medium_dark_skin_tone:': '🧚ðŸ¾â€â™‚ï¸', ':man_fairy_tone4:': '🧚ðŸ¾â€â™‚ï¸', ':man_fairy_dark_skin_tone:': '🧚ðŸ¿â€â™‚ï¸', ':man_fairy_tone5:': '🧚ðŸ¿â€â™‚ï¸', ':man_frowning_light_skin_tone:': 'ðŸ™ðŸ»â€â™‚ï¸', ':man_frowning_tone1:': 'ðŸ™ðŸ»â€â™‚ï¸', ':man_frowning_medium_light_skin_tone:': 'ðŸ™ðŸ¼â€â™‚ï¸', ':man_frowning_tone2:': 'ðŸ™ðŸ¼â€â™‚ï¸', ':man_frowning_medium_skin_tone:': 'ðŸ™ðŸ½â€â™‚ï¸', ':man_frowning_tone3:': 'ðŸ™ðŸ½â€â™‚ï¸', ':man_frowning_medium_dark_skin_tone:': 'ðŸ™ðŸ¾â€â™‚ï¸', ':man_frowning_tone4:': 'ðŸ™ðŸ¾â€â™‚ï¸', ':man_frowning_dark_skin_tone:': 'ðŸ™ðŸ¿â€â™‚ï¸', ':man_frowning_tone5:': 'ðŸ™ðŸ¿â€â™‚ï¸', ':man_gesturing_no_light_skin_tone:': '🙅ðŸ»â€â™‚ï¸', ':man_gesturing_no_tone1:': '🙅ðŸ»â€â™‚ï¸', ':man_gesturing_no_medium_light_skin_tone:': '🙅ðŸ¼â€â™‚ï¸', ':man_gesturing_no_tone2:': '🙅ðŸ¼â€â™‚ï¸', ':man_gesturing_no_medium_skin_tone:': '🙅ðŸ½â€â™‚ï¸', ':man_gesturing_no_tone3:': '🙅ðŸ½â€â™‚ï¸', ':man_gesturing_no_medium_dark_skin_tone:': '🙅ðŸ¾â€â™‚ï¸', ':man_gesturing_no_tone4:': '🙅ðŸ¾â€â™‚ï¸', ':man_gesturing_no_dark_skin_tone:': '🙅ðŸ¿â€â™‚ï¸', ':man_gesturing_no_tone5:': '🙅ðŸ¿â€â™‚ï¸', ':man_gesturing_ok_light_skin_tone:': '🙆ðŸ»â€â™‚ï¸', ':man_gesturing_ok_tone1:': '🙆ðŸ»â€â™‚ï¸', ':man_gesturing_ok_medium_light_skin_tone:': '🙆ðŸ¼â€â™‚ï¸', ':man_gesturing_ok_tone2:': '🙆ðŸ¼â€â™‚ï¸', ':man_gesturing_ok_medium_skin_tone:': '🙆ðŸ½â€â™‚ï¸', ':man_gesturing_ok_tone3:': '🙆ðŸ½â€â™‚ï¸', ':man_gesturing_ok_medium_dark_skin_tone:': '🙆ðŸ¾â€â™‚ï¸', ':man_gesturing_ok_tone4:': '🙆ðŸ¾â€â™‚ï¸', ':man_gesturing_ok_dark_skin_tone:': '🙆ðŸ¿â€â™‚ï¸', ':man_gesturing_ok_tone5:': '🙆ðŸ¿â€â™‚ï¸', ':man_getting_face_massage_light_skin_tone:': '💆ðŸ»â€â™‚ï¸', ':man_getting_face_massage_tone1:': '💆ðŸ»â€â™‚ï¸', ':man_getting_face_massage_medium_light_skin_tone:': '💆ðŸ¼â€â™‚ï¸', ':man_getting_face_massage_tone2:': '💆ðŸ¼â€â™‚ï¸', ':man_getting_face_massage_medium_skin_tone:': '💆ðŸ½â€â™‚ï¸', ':man_getting_face_massage_tone3:': '💆ðŸ½â€â™‚ï¸', ':man_getting_face_massage_medium_dark_skin_tone:': '💆ðŸ¾â€â™‚ï¸', ':man_getting_face_massage_tone4:': '💆ðŸ¾â€â™‚ï¸', ':man_getting_face_massage_dark_skin_tone:': '💆ðŸ¿â€â™‚ï¸', ':man_getting_face_massage_tone5:': '💆ðŸ¿â€â™‚ï¸', ':man_getting_haircut_light_skin_tone:': '💇ðŸ»â€â™‚ï¸', ':man_getting_haircut_tone1:': '💇ðŸ»â€â™‚ï¸', ':man_getting_haircut_medium_light_skin_tone:': '💇ðŸ¼â€â™‚ï¸', ':man_getting_haircut_tone2:': '💇ðŸ¼â€â™‚ï¸', ':man_getting_haircut_medium_skin_tone:': '💇ðŸ½â€â™‚ï¸', ':man_getting_haircut_tone3:': '💇ðŸ½â€â™‚ï¸', ':man_getting_haircut_medium_dark_skin_tone:': '💇ðŸ¾â€â™‚ï¸', ':man_getting_haircut_tone4:': '💇ðŸ¾â€â™‚ï¸', ':man_getting_haircut_dark_skin_tone:': '💇ðŸ¿â€â™‚ï¸', ':man_getting_haircut_tone5:': '💇ðŸ¿â€â™‚ï¸', ':man_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»â€â™‚ï¸', ':man_golfing_tone1:': 'ðŸŒï¸ðŸ»â€â™‚ï¸', ':man_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼â€â™‚ï¸', ':man_golfing_tone2:': 'ðŸŒï¸ðŸ¼â€â™‚ï¸', ':man_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½â€â™‚ï¸', ':man_golfing_tone3:': 'ðŸŒï¸ðŸ½â€â™‚ï¸', ':man_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾â€â™‚ï¸', ':man_golfing_tone4:': 'ðŸŒï¸ðŸ¾â€â™‚ï¸', ':man_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿â€â™‚ï¸', ':man_golfing_tone5:': 'ðŸŒï¸ðŸ¿â€â™‚ï¸', ':man_guard_light_skin_tone:': '💂ðŸ»â€â™‚ï¸', ':man_guard_tone1:': '💂ðŸ»â€â™‚ï¸', ':man_guard_medium_light_skin_tone:': '💂ðŸ¼â€â™‚ï¸', ':man_guard_tone2:': '💂ðŸ¼â€â™‚ï¸', ':man_guard_medium_skin_tone:': '💂ðŸ½â€â™‚ï¸', ':man_guard_tone3:': '💂ðŸ½â€â™‚ï¸', ':man_guard_medium_dark_skin_tone:': '💂ðŸ¾â€â™‚ï¸', ':man_guard_tone4:': '💂ðŸ¾â€â™‚ï¸', ':man_guard_dark_skin_tone:': '💂ðŸ¿â€â™‚ï¸', ':man_guard_tone5:': '💂ðŸ¿â€â™‚ï¸', ':man_health_worker_light_skin_tone:': '👨ðŸ»â€âš•ï¸', ':man_health_worker_tone1:': '👨ðŸ»â€âš•ï¸', ':man_health_worker_medium_light_skin_tone:': '👨ðŸ¼â€âš•ï¸', ':man_health_worker_tone2:': '👨ðŸ¼â€âš•ï¸', ':man_health_worker_medium_skin_tone:': '👨ðŸ½â€âš•ï¸', ':man_health_worker_tone3:': '👨ðŸ½â€âš•ï¸', ':man_health_worker_medium_dark_skin_tone:': '👨ðŸ¾â€âš•ï¸', ':man_health_worker_tone4:': '👨ðŸ¾â€âš•ï¸', ':man_health_worker_dark_skin_tone:': '👨ðŸ¿â€âš•ï¸', ':man_health_worker_tone5:': '👨ðŸ¿â€âš•ï¸', ':man_in_lotus_position_light_skin_tone:': '🧘ðŸ»â€â™‚ï¸', ':man_in_lotus_position_tone1:': '🧘ðŸ»â€â™‚ï¸', ':man_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼â€â™‚ï¸', ':man_in_lotus_position_tone2:': '🧘ðŸ¼â€â™‚ï¸', ':man_in_lotus_position_medium_skin_tone:': '🧘ðŸ½â€â™‚ï¸', ':man_in_lotus_position_tone3:': '🧘ðŸ½â€â™‚ï¸', ':man_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾â€â™‚ï¸', ':man_in_lotus_position_tone4:': '🧘ðŸ¾â€â™‚ï¸', ':man_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿â€â™‚ï¸', ':man_in_lotus_position_tone5:': '🧘ðŸ¿â€â™‚ï¸', ':man_in_steamy_room_light_skin_tone:': '🧖ðŸ»â€â™‚ï¸', ':man_in_steamy_room_tone1:': '🧖ðŸ»â€â™‚ï¸', ':man_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼â€â™‚ï¸', ':man_in_steamy_room_tone2:': '🧖ðŸ¼â€â™‚ï¸', ':man_in_steamy_room_medium_skin_tone:': '🧖ðŸ½â€â™‚ï¸', ':man_in_steamy_room_tone3:': '🧖ðŸ½â€â™‚ï¸', ':man_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾â€â™‚ï¸', ':man_in_steamy_room_tone4:': '🧖ðŸ¾â€â™‚ï¸', ':man_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿â€â™‚ï¸', ':man_in_steamy_room_tone5:': '🧖ðŸ¿â€â™‚ï¸', ':man_judge_light_skin_tone:': '👨ðŸ»â€âš–ï¸', ':man_judge_tone1:': '👨ðŸ»â€âš–ï¸', ':man_judge_medium_light_skin_tone:': '👨ðŸ¼â€âš–ï¸', ':man_judge_tone2:': '👨ðŸ¼â€âš–ï¸', ':man_judge_medium_skin_tone:': '👨ðŸ½â€âš–ï¸', ':man_judge_tone3:': '👨ðŸ½â€âš–ï¸', ':man_judge_medium_dark_skin_tone:': '👨ðŸ¾â€âš–ï¸', ':man_judge_tone4:': '👨ðŸ¾â€âš–ï¸', ':man_judge_dark_skin_tone:': '👨ðŸ¿â€âš–ï¸', ':man_judge_tone5:': '👨ðŸ¿â€âš–ï¸', ':man_juggling_light_skin_tone:': '🤹ðŸ»â€â™‚ï¸', ':man_juggling_tone1:': '🤹ðŸ»â€â™‚ï¸', ':man_juggling_medium_light_skin_tone:': '🤹ðŸ¼â€â™‚ï¸', ':man_juggling_tone2:': '🤹ðŸ¼â€â™‚ï¸', ':man_juggling_medium_skin_tone:': '🤹ðŸ½â€â™‚ï¸', ':man_juggling_tone3:': '🤹ðŸ½â€â™‚ï¸', ':man_juggling_medium_dark_skin_tone:': '🤹ðŸ¾â€â™‚ï¸', ':man_juggling_tone4:': '🤹ðŸ¾â€â™‚ï¸', ':man_juggling_dark_skin_tone:': '🤹ðŸ¿â€â™‚ï¸', ':man_juggling_tone5:': '🤹ðŸ¿â€â™‚ï¸', ':man_kneeling_light_skin_tone:': '🧎ðŸ»â€â™‚ï¸', ':man_kneeling_tone1:': '🧎ðŸ»â€â™‚ï¸', ':man_kneeling_medium_light_skin_tone:': '🧎ðŸ¼â€â™‚ï¸', ':man_kneeling_tone2:': '🧎ðŸ¼â€â™‚ï¸', ':man_kneeling_medium_skin_tone:': '🧎ðŸ½â€â™‚ï¸', ':man_kneeling_tone3:': '🧎ðŸ½â€â™‚ï¸', ':man_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾â€â™‚ï¸', ':man_kneeling_tone4:': '🧎ðŸ¾â€â™‚ï¸', ':man_kneeling_dark_skin_tone:': '🧎ðŸ¿â€â™‚ï¸', ':man_kneeling_tone5:': '🧎ðŸ¿â€â™‚ï¸', ':man_lifting_weights_light_skin_tone:': 'ðŸ‹ï¸ðŸ»â€â™‚ï¸', ':man_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»â€â™‚ï¸', ':man_lifting_weights_medium_light_skin_tone:': 'ðŸ‹ï¸ðŸ¼â€â™‚ï¸', ':man_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼â€â™‚ï¸', ':man_lifting_weights_medium_skin_tone:': 'ðŸ‹ï¸ðŸ½â€â™‚ï¸', ':man_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½â€â™‚ï¸', ':man_lifting_weights_medium_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¾â€â™‚ï¸', ':man_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾â€â™‚ï¸', ':man_lifting_weights_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¿â€â™‚ï¸', ':man_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿â€â™‚ï¸', ':man_mage_light_skin_tone:': '🧙ðŸ»â€â™‚ï¸', ':man_mage_tone1:': '🧙ðŸ»â€â™‚ï¸', ':man_mage_medium_light_skin_tone:': '🧙ðŸ¼â€â™‚ï¸', ':man_mage_tone2:': '🧙ðŸ¼â€â™‚ï¸', ':man_mage_medium_skin_tone:': '🧙ðŸ½â€â™‚ï¸', ':man_mage_tone3:': '🧙ðŸ½â€â™‚ï¸', ':man_mage_medium_dark_skin_tone:': '🧙ðŸ¾â€â™‚ï¸', ':man_mage_tone4:': '🧙ðŸ¾â€â™‚ï¸', ':man_mage_dark_skin_tone:': '🧙ðŸ¿â€â™‚ï¸', ':man_mage_tone5:': '🧙ðŸ¿â€â™‚ï¸', ':man_mountain_biking_light_skin_tone:': '🚵ðŸ»â€â™‚ï¸', ':man_mountain_biking_tone1:': '🚵ðŸ»â€â™‚ï¸', ':man_mountain_biking_medium_light_skin_tone:': '🚵ðŸ¼â€â™‚ï¸', ':man_mountain_biking_tone2:': '🚵ðŸ¼â€â™‚ï¸', ':man_mountain_biking_medium_skin_tone:': '🚵ðŸ½â€â™‚ï¸', ':man_mountain_biking_tone3:': '🚵ðŸ½â€â™‚ï¸', ':man_mountain_biking_medium_dark_skin_tone:': '🚵ðŸ¾â€â™‚ï¸', ':man_mountain_biking_tone4:': '🚵ðŸ¾â€â™‚ï¸', ':man_mountain_biking_dark_skin_tone:': '🚵ðŸ¿â€â™‚ï¸', ':man_mountain_biking_tone5:': '🚵ðŸ¿â€â™‚ï¸', ':man_pilot_light_skin_tone:': '👨ðŸ»â€âœˆï¸', ':man_pilot_tone1:': '👨ðŸ»â€âœˆï¸', ':man_pilot_medium_light_skin_tone:': '👨ðŸ¼â€âœˆï¸', ':man_pilot_tone2:': '👨ðŸ¼â€âœˆï¸', ':man_pilot_medium_skin_tone:': '👨ðŸ½â€âœˆï¸', ':man_pilot_tone3:': '👨ðŸ½â€âœˆï¸', ':man_pilot_medium_dark_skin_tone:': '👨ðŸ¾â€âœˆï¸', ':man_pilot_tone4:': '👨ðŸ¾â€âœˆï¸', ':man_pilot_dark_skin_tone:': '👨ðŸ¿â€âœˆï¸', ':man_pilot_tone5:': '👨ðŸ¿â€âœˆï¸', ':man_playing_handball_light_skin_tone:': '🤾ðŸ»â€â™‚ï¸', ':man_playing_handball_tone1:': '🤾ðŸ»â€â™‚ï¸', ':man_playing_handball_medium_light_skin_tone:': '🤾ðŸ¼â€â™‚ï¸', ':man_playing_handball_tone2:': '🤾ðŸ¼â€â™‚ï¸', ':man_playing_handball_medium_skin_tone:': '🤾ðŸ½â€â™‚ï¸', ':man_playing_handball_tone3:': '🤾ðŸ½â€â™‚ï¸', ':man_playing_handball_medium_dark_skin_tone:': '🤾ðŸ¾â€â™‚ï¸', ':man_playing_handball_tone4:': '🤾ðŸ¾â€â™‚ï¸', ':man_playing_handball_dark_skin_tone:': '🤾ðŸ¿â€â™‚ï¸', ':man_playing_handball_tone5:': '🤾ðŸ¿â€â™‚ï¸', ':man_playing_water_polo_light_skin_tone:': '🤽ðŸ»â€â™‚ï¸', ':man_playing_water_polo_tone1:': '🤽ðŸ»â€â™‚ï¸', ':man_playing_water_polo_medium_light_skin_tone:': '🤽ðŸ¼â€â™‚ï¸', ':man_playing_water_polo_tone2:': '🤽ðŸ¼â€â™‚ï¸', ':man_playing_water_polo_medium_skin_tone:': '🤽ðŸ½â€â™‚ï¸', ':man_playing_water_polo_tone3:': '🤽ðŸ½â€â™‚ï¸', ':man_playing_water_polo_medium_dark_skin_tone:': '🤽ðŸ¾â€â™‚ï¸', ':man_playing_water_polo_tone4:': '🤽ðŸ¾â€â™‚ï¸', ':man_playing_water_polo_dark_skin_tone:': '🤽ðŸ¿â€â™‚ï¸', ':man_playing_water_polo_tone5:': '🤽ðŸ¿â€â™‚ï¸', ':man_police_officer_light_skin_tone:': '👮ðŸ»â€â™‚ï¸', ':man_police_officer_tone1:': '👮ðŸ»â€â™‚ï¸', ':man_police_officer_medium_light_skin_tone:': '👮ðŸ¼â€â™‚ï¸', ':man_police_officer_tone2:': '👮ðŸ¼â€â™‚ï¸', ':man_police_officer_medium_skin_tone:': '👮ðŸ½â€â™‚ï¸', ':man_police_officer_tone3:': '👮ðŸ½â€â™‚ï¸', ':man_police_officer_medium_dark_skin_tone:': '👮ðŸ¾â€â™‚ï¸', ':man_police_officer_tone4:': '👮ðŸ¾â€â™‚ï¸', ':man_police_officer_dark_skin_tone:': '👮ðŸ¿â€â™‚ï¸', ':man_police_officer_tone5:': '👮ðŸ¿â€â™‚ï¸', ':man_pouting_light_skin_tone:': '🙎ðŸ»â€â™‚ï¸', ':man_pouting_tone1:': '🙎ðŸ»â€â™‚ï¸', ':man_pouting_medium_light_skin_tone:': '🙎ðŸ¼â€â™‚ï¸', ':man_pouting_tone2:': '🙎ðŸ¼â€â™‚ï¸', ':man_pouting_medium_skin_tone:': '🙎ðŸ½â€â™‚ï¸', ':man_pouting_tone3:': '🙎ðŸ½â€â™‚ï¸', ':man_pouting_medium_dark_skin_tone:': '🙎ðŸ¾â€â™‚ï¸', ':man_pouting_tone4:': '🙎ðŸ¾â€â™‚ï¸', ':man_pouting_dark_skin_tone:': '🙎ðŸ¿â€â™‚ï¸', ':man_pouting_tone5:': '🙎ðŸ¿â€â™‚ï¸', ':man_raising_hand_light_skin_tone:': '🙋ðŸ»â€â™‚ï¸', ':man_raising_hand_tone1:': '🙋ðŸ»â€â™‚ï¸', ':man_raising_hand_medium_light_skin_tone:': '🙋ðŸ¼â€â™‚ï¸', ':man_raising_hand_tone2:': '🙋ðŸ¼â€â™‚ï¸', ':man_raising_hand_medium_skin_tone:': '🙋ðŸ½â€â™‚ï¸', ':man_raising_hand_tone3:': '🙋ðŸ½â€â™‚ï¸', ':man_raising_hand_medium_dark_skin_tone:': '🙋ðŸ¾â€â™‚ï¸', ':man_raising_hand_tone4:': '🙋ðŸ¾â€â™‚ï¸', ':man_raising_hand_dark_skin_tone:': '🙋ðŸ¿â€â™‚ï¸', ':man_raising_hand_tone5:': '🙋ðŸ¿â€â™‚ï¸', ':man_rowing_boat_light_skin_tone:': '🚣ðŸ»â€â™‚ï¸', ':man_rowing_boat_tone1:': '🚣ðŸ»â€â™‚ï¸', ':man_rowing_boat_medium_light_skin_tone:': '🚣ðŸ¼â€â™‚ï¸', ':man_rowing_boat_tone2:': '🚣ðŸ¼â€â™‚ï¸', ':man_rowing_boat_medium_skin_tone:': '🚣ðŸ½â€â™‚ï¸', ':man_rowing_boat_tone3:': '🚣ðŸ½â€â™‚ï¸', ':man_rowing_boat_medium_dark_skin_tone:': '🚣ðŸ¾â€â™‚ï¸', ':man_rowing_boat_tone4:': '🚣ðŸ¾â€â™‚ï¸', ':man_rowing_boat_dark_skin_tone:': '🚣ðŸ¿â€â™‚ï¸', ':man_rowing_boat_tone5:': '🚣ðŸ¿â€â™‚ï¸', ':man_running_light_skin_tone:': 'ðŸƒðŸ»â€â™‚ï¸', ':man_running_tone1:': 'ðŸƒðŸ»â€â™‚ï¸', ':man_running_medium_light_skin_tone:': 'ðŸƒðŸ¼â€â™‚ï¸', ':man_running_tone2:': 'ðŸƒðŸ¼â€â™‚ï¸', ':man_running_medium_skin_tone:': 'ðŸƒðŸ½â€â™‚ï¸', ':man_running_tone3:': 'ðŸƒðŸ½â€â™‚ï¸', ':man_running_medium_dark_skin_tone:': 'ðŸƒðŸ¾â€â™‚ï¸', ':man_running_tone4:': 'ðŸƒðŸ¾â€â™‚ï¸', ':man_running_dark_skin_tone:': 'ðŸƒðŸ¿â€â™‚ï¸', ':man_running_tone5:': 'ðŸƒðŸ¿â€â™‚ï¸', ':man_shrugging_light_skin_tone:': '🤷ðŸ»â€â™‚ï¸', ':man_shrugging_tone1:': '🤷ðŸ»â€â™‚ï¸', ':man_shrugging_medium_light_skin_tone:': '🤷ðŸ¼â€â™‚ï¸', ':man_shrugging_tone2:': '🤷ðŸ¼â€â™‚ï¸', ':man_shrugging_medium_skin_tone:': '🤷ðŸ½â€â™‚ï¸', ':man_shrugging_tone3:': '🤷ðŸ½â€â™‚ï¸', ':man_shrugging_medium_dark_skin_tone:': '🤷ðŸ¾â€â™‚ï¸', ':man_shrugging_tone4:': '🤷ðŸ¾â€â™‚ï¸', ':man_shrugging_dark_skin_tone:': '🤷ðŸ¿â€â™‚ï¸', ':man_shrugging_tone5:': '🤷ðŸ¿â€â™‚ï¸', ':man_standing_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', ':man_standing_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', ':man_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':man_standing_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', ':man_standing_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', ':man_standing_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', ':man_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':man_standing_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', ':man_standing_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':man_standing_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', ':man_superhero_light_skin_tone:': '🦸ðŸ»â€â™‚ï¸', ':man_superhero_tone1:': '🦸ðŸ»â€â™‚ï¸', ':man_superhero_medium_light_skin_tone:': '🦸ðŸ¼â€â™‚ï¸', ':man_superhero_tone2:': '🦸ðŸ¼â€â™‚ï¸', ':man_superhero_medium_skin_tone:': '🦸ðŸ½â€â™‚ï¸', ':man_superhero_tone3:': '🦸ðŸ½â€â™‚ï¸', ':man_superhero_medium_dark_skin_tone:': '🦸ðŸ¾â€â™‚ï¸', ':man_superhero_tone4:': '🦸ðŸ¾â€â™‚ï¸', ':man_superhero_dark_skin_tone:': '🦸ðŸ¿â€â™‚ï¸', ':man_superhero_tone5:': '🦸ðŸ¿â€â™‚ï¸', ':man_supervillain_light_skin_tone:': '🦹ðŸ»â€â™‚ï¸', ':man_supervillain_tone1:': '🦹ðŸ»â€â™‚ï¸', ':man_supervillain_medium_light_skin_tone:': '🦹ðŸ¼â€â™‚ï¸', ':man_supervillain_tone2:': '🦹ðŸ¼â€â™‚ï¸', ':man_supervillain_medium_skin_tone:': '🦹ðŸ½â€â™‚ï¸', ':man_supervillain_tone3:': '🦹ðŸ½â€â™‚ï¸', ':man_supervillain_medium_dark_skin_tone:': '🦹ðŸ¾â€â™‚ï¸', ':man_supervillain_tone4:': '🦹ðŸ¾â€â™‚ï¸', ':man_supervillain_dark_skin_tone:': '🦹ðŸ¿â€â™‚ï¸', ':man_supervillain_tone5:': '🦹ðŸ¿â€â™‚ï¸', ':man_surfing_light_skin_tone:': 'ðŸ„ðŸ»â€â™‚ï¸', ':man_surfing_tone1:': 'ðŸ„ðŸ»â€â™‚ï¸', ':man_surfing_medium_light_skin_tone:': 'ðŸ„ðŸ¼â€â™‚ï¸', ':man_surfing_tone2:': 'ðŸ„ðŸ¼â€â™‚ï¸', ':man_surfing_medium_skin_tone:': 'ðŸ„ðŸ½â€â™‚ï¸', ':man_surfing_tone3:': 'ðŸ„ðŸ½â€â™‚ï¸', ':man_surfing_medium_dark_skin_tone:': 'ðŸ„ðŸ¾â€â™‚ï¸', ':man_surfing_tone4:': 'ðŸ„ðŸ¾â€â™‚ï¸', ':man_surfing_dark_skin_tone:': 'ðŸ„ðŸ¿â€â™‚ï¸', ':man_surfing_tone5:': 'ðŸ„ðŸ¿â€â™‚ï¸', ':man_swimming_light_skin_tone:': 'ðŸŠðŸ»â€â™‚ï¸', ':man_swimming_tone1:': 'ðŸŠðŸ»â€â™‚ï¸', ':man_swimming_medium_light_skin_tone:': 'ðŸŠðŸ¼â€â™‚ï¸', ':man_swimming_tone2:': 'ðŸŠðŸ¼â€â™‚ï¸', ':man_swimming_medium_skin_tone:': 'ðŸŠðŸ½â€â™‚ï¸', ':man_swimming_tone3:': 'ðŸŠðŸ½â€â™‚ï¸', ':man_swimming_medium_dark_skin_tone:': 'ðŸŠðŸ¾â€â™‚ï¸', ':man_swimming_tone4:': 'ðŸŠðŸ¾â€â™‚ï¸', ':man_swimming_dark_skin_tone:': 'ðŸŠðŸ¿â€â™‚ï¸', ':man_swimming_tone5:': 'ðŸŠðŸ¿â€â™‚ï¸', ':man_tipping_hand_light_skin_tone:': 'ðŸ’ðŸ»â€â™‚ï¸', ':man_tipping_hand_tone1:': 'ðŸ’ðŸ»â€â™‚ï¸', ':man_tipping_hand_medium_light_skin_tone:': 'ðŸ’ðŸ¼â€â™‚ï¸', ':man_tipping_hand_tone2:': 'ðŸ’ðŸ¼â€â™‚ï¸', ':man_tipping_hand_medium_skin_tone:': 'ðŸ’ðŸ½â€â™‚ï¸', ':man_tipping_hand_tone3:': 'ðŸ’ðŸ½â€â™‚ï¸', ':man_tipping_hand_medium_dark_skin_tone:': 'ðŸ’ðŸ¾â€â™‚ï¸', ':man_tipping_hand_tone4:': 'ðŸ’ðŸ¾â€â™‚ï¸', ':man_tipping_hand_dark_skin_tone:': 'ðŸ’ðŸ¿â€â™‚ï¸', ':man_tipping_hand_tone5:': 'ðŸ’ðŸ¿â€â™‚ï¸', ':man_vampire_light_skin_tone:': '🧛ðŸ»â€â™‚ï¸', ':man_vampire_tone1:': '🧛ðŸ»â€â™‚ï¸', ':man_vampire_medium_light_skin_tone:': '🧛ðŸ¼â€â™‚ï¸', ':man_vampire_tone2:': '🧛ðŸ¼â€â™‚ï¸', ':man_vampire_medium_skin_tone:': '🧛ðŸ½â€â™‚ï¸', ':man_vampire_tone3:': '🧛ðŸ½â€â™‚ï¸', ':man_vampire_medium_dark_skin_tone:': '🧛ðŸ¾â€â™‚ï¸', ':man_vampire_tone4:': '🧛ðŸ¾â€â™‚ï¸', ':man_vampire_dark_skin_tone:': '🧛ðŸ¿â€â™‚ï¸', ':man_vampire_tone5:': '🧛ðŸ¿â€â™‚ï¸', ':man_walking_light_skin_tone:': '🚶ðŸ»â€â™‚ï¸', ':man_walking_tone1:': '🚶ðŸ»â€â™‚ï¸', ':man_walking_medium_light_skin_tone:': '🚶ðŸ¼â€â™‚ï¸', ':man_walking_tone2:': '🚶ðŸ¼â€â™‚ï¸', ':man_walking_medium_skin_tone:': '🚶ðŸ½â€â™‚ï¸', ':man_walking_tone3:': '🚶ðŸ½â€â™‚ï¸', ':man_walking_medium_dark_skin_tone:': '🚶ðŸ¾â€â™‚ï¸', ':man_walking_tone4:': '🚶ðŸ¾â€â™‚ï¸', ':man_walking_dark_skin_tone:': '🚶ðŸ¿â€â™‚ï¸', ':man_walking_tone5:': '🚶ðŸ¿â€â™‚ï¸', ':man_wearing_turban_light_skin_tone:': '👳ðŸ»â€â™‚ï¸', ':man_wearing_turban_tone1:': '👳ðŸ»â€â™‚ï¸', ':man_wearing_turban_medium_light_skin_tone:': '👳ðŸ¼â€â™‚ï¸', ':man_wearing_turban_tone2:': '👳ðŸ¼â€â™‚ï¸', ':man_wearing_turban_medium_skin_tone:': '👳ðŸ½â€â™‚ï¸', ':man_wearing_turban_tone3:': '👳ðŸ½â€â™‚ï¸', ':man_wearing_turban_medium_dark_skin_tone:': '👳ðŸ¾â€â™‚ï¸', ':man_wearing_turban_tone4:': '👳ðŸ¾â€â™‚ï¸', ':man_wearing_turban_dark_skin_tone:': '👳ðŸ¿â€â™‚ï¸', ':man_wearing_turban_tone5:': '👳ðŸ¿â€â™‚ï¸', ':mermaid_light_skin_tone:': '🧜ðŸ»â€â™€ï¸', ':mermaid_tone1:': '🧜ðŸ»â€â™€ï¸', ':mermaid_medium_light_skin_tone:': '🧜ðŸ¼â€â™€ï¸', ':mermaid_tone2:': '🧜ðŸ¼â€â™€ï¸', ':mermaid_medium_skin_tone:': '🧜ðŸ½â€â™€ï¸', ':mermaid_tone3:': '🧜ðŸ½â€â™€ï¸', ':mermaid_medium_dark_skin_tone:': '🧜ðŸ¾â€â™€ï¸', ':mermaid_tone4:': '🧜ðŸ¾â€â™€ï¸', ':mermaid_dark_skin_tone:': '🧜ðŸ¿â€â™€ï¸', ':mermaid_tone5:': '🧜ðŸ¿â€â™€ï¸', ':merman_light_skin_tone:': '🧜ðŸ»â€â™‚ï¸', ':merman_tone1:': '🧜ðŸ»â€â™‚ï¸', ':merman_medium_light_skin_tone:': '🧜ðŸ¼â€â™‚ï¸', ':merman_tone2:': '🧜ðŸ¼â€â™‚ï¸', ':merman_medium_skin_tone:': '🧜ðŸ½â€â™‚ï¸', ':merman_tone3:': '🧜ðŸ½â€â™‚ï¸', ':merman_medium_dark_skin_tone:': '🧜ðŸ¾â€â™‚ï¸', ':merman_tone4:': '🧜ðŸ¾â€â™‚ï¸', ':merman_dark_skin_tone:': '🧜ðŸ¿â€â™‚ï¸', ':merman_tone5:': '🧜ðŸ¿â€â™‚ï¸', ':woman_biking_light_skin_tone:': '🚴ðŸ»â€â™€ï¸', ':woman_biking_tone1:': '🚴ðŸ»â€â™€ï¸', ':woman_biking_medium_light_skin_tone:': '🚴ðŸ¼â€â™€ï¸', ':woman_biking_tone2:': '🚴ðŸ¼â€â™€ï¸', ':woman_biking_medium_skin_tone:': '🚴ðŸ½â€â™€ï¸', ':woman_biking_tone3:': '🚴ðŸ½â€â™€ï¸', ':woman_biking_medium_dark_skin_tone:': '🚴ðŸ¾â€â™€ï¸', ':woman_biking_tone4:': '🚴ðŸ¾â€â™€ï¸', ':woman_biking_dark_skin_tone:': '🚴ðŸ¿â€â™€ï¸', ':woman_biking_tone5:': '🚴ðŸ¿â€â™€ï¸', ':woman_bowing_light_skin_tone:': '🙇ðŸ»â€â™€ï¸', ':woman_bowing_tone1:': '🙇ðŸ»â€â™€ï¸', ':woman_bowing_medium_light_skin_tone:': '🙇ðŸ¼â€â™€ï¸', ':woman_bowing_tone2:': '🙇ðŸ¼â€â™€ï¸', ':woman_bowing_medium_skin_tone:': '🙇ðŸ½â€â™€ï¸', ':woman_bowing_tone3:': '🙇ðŸ½â€â™€ï¸', ':woman_bowing_medium_dark_skin_tone:': '🙇ðŸ¾â€â™€ï¸', ':woman_bowing_tone4:': '🙇ðŸ¾â€â™€ï¸', ':woman_bowing_dark_skin_tone:': '🙇ðŸ¿â€â™€ï¸', ':woman_bowing_tone5:': '🙇ðŸ¿â€â™€ï¸', ':woman_cartwheeling_light_skin_tone:': '🤸ðŸ»â€â™€ï¸', ':woman_cartwheeling_tone1:': '🤸ðŸ»â€â™€ï¸', ':woman_cartwheeling_medium_light_skin_tone:': '🤸ðŸ¼â€â™€ï¸', ':woman_cartwheeling_tone2:': '🤸ðŸ¼â€â™€ï¸', ':woman_cartwheeling_medium_skin_tone:': '🤸ðŸ½â€â™€ï¸', ':woman_cartwheeling_tone3:': '🤸ðŸ½â€â™€ï¸', ':woman_cartwheeling_medium_dark_skin_tone:': '🤸ðŸ¾â€â™€ï¸', ':woman_cartwheeling_tone4:': '🤸ðŸ¾â€â™€ï¸', ':woman_cartwheeling_dark_skin_tone:': '🤸ðŸ¿â€â™€ï¸', ':woman_cartwheeling_tone5:': '🤸ðŸ¿â€â™€ï¸', ':woman_climbing_light_skin_tone:': '🧗ðŸ»â€â™€ï¸', ':woman_climbing_tone1:': '🧗ðŸ»â€â™€ï¸', ':woman_climbing_medium_light_skin_tone:': '🧗ðŸ¼â€â™€ï¸', ':woman_climbing_tone2:': '🧗ðŸ¼â€â™€ï¸', ':woman_climbing_medium_skin_tone:': '🧗ðŸ½â€â™€ï¸', ':woman_climbing_tone3:': '🧗ðŸ½â€â™€ï¸', ':woman_climbing_medium_dark_skin_tone:': '🧗ðŸ¾â€â™€ï¸', ':woman_climbing_tone4:': '🧗ðŸ¾â€â™€ï¸', ':woman_climbing_dark_skin_tone:': '🧗ðŸ¿â€â™€ï¸', ':woman_climbing_tone5:': '🧗ðŸ¿â€â™€ï¸', ':woman_construction_worker_light_skin_tone:': '👷ðŸ»â€â™€ï¸', ':woman_construction_worker_tone1:': '👷ðŸ»â€â™€ï¸', ':woman_construction_worker_medium_light_skin_tone:': '👷ðŸ¼â€â™€ï¸', ':woman_construction_worker_tone2:': '👷ðŸ¼â€â™€ï¸', ':woman_construction_worker_medium_skin_tone:': '👷ðŸ½â€â™€ï¸', ':woman_construction_worker_tone3:': '👷ðŸ½â€â™€ï¸', ':woman_construction_worker_medium_dark_skin_tone:': '👷ðŸ¾â€â™€ï¸', ':woman_construction_worker_tone4:': '👷ðŸ¾â€â™€ï¸', ':woman_construction_worker_dark_skin_tone:': '👷ðŸ¿â€â™€ï¸', ':woman_construction_worker_tone5:': '👷ðŸ¿â€â™€ï¸', ':woman_detective_light_skin_tone:': '🕵ï¸ðŸ»â€â™€ï¸', ':woman_detective_tone1:': '🕵ï¸ðŸ»â€â™€ï¸', ':woman_detective_medium_light_skin_tone:': '🕵ï¸ðŸ¼â€â™€ï¸', ':woman_detective_tone2:': '🕵ï¸ðŸ¼â€â™€ï¸', ':woman_detective_medium_skin_tone:': '🕵ï¸ðŸ½â€â™€ï¸', ':woman_detective_tone3:': '🕵ï¸ðŸ½â€â™€ï¸', ':woman_detective_medium_dark_skin_tone:': '🕵ï¸ðŸ¾â€â™€ï¸', ':woman_detective_tone4:': '🕵ï¸ðŸ¾â€â™€ï¸', ':woman_detective_dark_skin_tone:': '🕵ï¸ðŸ¿â€â™€ï¸', ':woman_detective_tone5:': '🕵ï¸ðŸ¿â€â™€ï¸', ':woman_elf_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', ':woman_elf_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', ':woman_elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', ':woman_elf_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', ':woman_elf_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', ':woman_elf_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', ':woman_elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', ':woman_elf_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', ':woman_elf_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', ':woman_elf_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', ':woman_facepalming_light_skin_tone:': '🤦ðŸ»â€â™€ï¸', ':woman_facepalming_tone1:': '🤦ðŸ»â€â™€ï¸', ':woman_facepalming_medium_light_skin_tone:': '🤦ðŸ¼â€â™€ï¸', ':woman_facepalming_tone2:': '🤦ðŸ¼â€â™€ï¸', ':woman_facepalming_medium_skin_tone:': '🤦ðŸ½â€â™€ï¸', ':woman_facepalming_tone3:': '🤦ðŸ½â€â™€ï¸', ':woman_facepalming_medium_dark_skin_tone:': '🤦ðŸ¾â€â™€ï¸', ':woman_facepalming_tone4:': '🤦ðŸ¾â€â™€ï¸', ':woman_facepalming_dark_skin_tone:': '🤦ðŸ¿â€â™€ï¸', ':woman_facepalming_tone5:': '🤦ðŸ¿â€â™€ï¸', ':woman_fairy_light_skin_tone:': '🧚ðŸ»â€â™€ï¸', ':woman_fairy_tone1:': '🧚ðŸ»â€â™€ï¸', ':woman_fairy_medium_light_skin_tone:': '🧚ðŸ¼â€â™€ï¸', ':woman_fairy_tone2:': '🧚ðŸ¼â€â™€ï¸', ':woman_fairy_medium_skin_tone:': '🧚ðŸ½â€â™€ï¸', ':woman_fairy_tone3:': '🧚ðŸ½â€â™€ï¸', ':woman_fairy_medium_dark_skin_tone:': '🧚ðŸ¾â€â™€ï¸', ':woman_fairy_tone4:': '🧚ðŸ¾â€â™€ï¸', ':woman_fairy_dark_skin_tone:': '🧚ðŸ¿â€â™€ï¸', ':woman_fairy_tone5:': '🧚ðŸ¿â€â™€ï¸', ':woman_frowning_light_skin_tone:': 'ðŸ™ðŸ»â€â™€ï¸', ':woman_frowning_tone1:': 'ðŸ™ðŸ»â€â™€ï¸', ':woman_frowning_medium_light_skin_tone:': 'ðŸ™ðŸ¼â€â™€ï¸', ':woman_frowning_tone2:': 'ðŸ™ðŸ¼â€â™€ï¸', ':woman_frowning_medium_skin_tone:': 'ðŸ™ðŸ½â€â™€ï¸', ':woman_frowning_tone3:': 'ðŸ™ðŸ½â€â™€ï¸', ':woman_frowning_medium_dark_skin_tone:': 'ðŸ™ðŸ¾â€â™€ï¸', ':woman_frowning_tone4:': 'ðŸ™ðŸ¾â€â™€ï¸', ':woman_frowning_dark_skin_tone:': 'ðŸ™ðŸ¿â€â™€ï¸', ':woman_frowning_tone5:': 'ðŸ™ðŸ¿â€â™€ï¸', ':woman_gesturing_no_light_skin_tone:': '🙅ðŸ»â€â™€ï¸', ':woman_gesturing_no_tone1:': '🙅ðŸ»â€â™€ï¸', ':woman_gesturing_no_medium_light_skin_tone:': '🙅ðŸ¼â€â™€ï¸', ':woman_gesturing_no_tone2:': '🙅ðŸ¼â€â™€ï¸', ':woman_gesturing_no_medium_skin_tone:': '🙅ðŸ½â€â™€ï¸', ':woman_gesturing_no_tone3:': '🙅ðŸ½â€â™€ï¸', ':woman_gesturing_no_medium_dark_skin_tone:': '🙅ðŸ¾â€â™€ï¸', ':woman_gesturing_no_tone4:': '🙅ðŸ¾â€â™€ï¸', ':woman_gesturing_no_dark_skin_tone:': '🙅ðŸ¿â€â™€ï¸', ':woman_gesturing_no_tone5:': '🙅ðŸ¿â€â™€ï¸', ':woman_gesturing_ok_light_skin_tone:': '🙆ðŸ»â€â™€ï¸', ':woman_gesturing_ok_tone1:': '🙆ðŸ»â€â™€ï¸', ':woman_gesturing_ok_medium_light_skin_tone:': '🙆ðŸ¼â€â™€ï¸', ':woman_gesturing_ok_tone2:': '🙆ðŸ¼â€â™€ï¸', ':woman_gesturing_ok_medium_skin_tone:': '🙆ðŸ½â€â™€ï¸', ':woman_gesturing_ok_tone3:': '🙆ðŸ½â€â™€ï¸', ':woman_gesturing_ok_medium_dark_skin_tone:': '🙆ðŸ¾â€â™€ï¸', ':woman_gesturing_ok_tone4:': '🙆ðŸ¾â€â™€ï¸', ':woman_gesturing_ok_dark_skin_tone:': '🙆ðŸ¿â€â™€ï¸', ':woman_gesturing_ok_tone5:': '🙆ðŸ¿â€â™€ï¸', ':woman_getting_face_massage_light_skin_tone:': '💆ðŸ»â€â™€ï¸', ':woman_getting_face_massage_tone1:': '💆ðŸ»â€â™€ï¸', ':woman_getting_face_massage_medium_light_skin_tone:': '💆ðŸ¼â€â™€ï¸', ':woman_getting_face_massage_tone2:': '💆ðŸ¼â€â™€ï¸', ':woman_getting_face_massage_medium_skin_tone:': '💆ðŸ½â€â™€ï¸', ':woman_getting_face_massage_tone3:': '💆ðŸ½â€â™€ï¸', ':woman_getting_face_massage_medium_dark_skin_tone:': '💆ðŸ¾â€â™€ï¸', ':woman_getting_face_massage_tone4:': '💆ðŸ¾â€â™€ï¸', ':woman_getting_face_massage_dark_skin_tone:': '💆ðŸ¿â€â™€ï¸', ':woman_getting_face_massage_tone5:': '💆ðŸ¿â€â™€ï¸', ':woman_getting_haircut_light_skin_tone:': '💇ðŸ»â€â™€ï¸', ':woman_getting_haircut_tone1:': '💇ðŸ»â€â™€ï¸', ':woman_getting_haircut_medium_light_skin_tone:': '💇ðŸ¼â€â™€ï¸', ':woman_getting_haircut_tone2:': '💇ðŸ¼â€â™€ï¸', ':woman_getting_haircut_medium_skin_tone:': '💇ðŸ½â€â™€ï¸', ':woman_getting_haircut_tone3:': '💇ðŸ½â€â™€ï¸', ':woman_getting_haircut_medium_dark_skin_tone:': '💇ðŸ¾â€â™€ï¸', ':woman_getting_haircut_tone4:': '💇ðŸ¾â€â™€ï¸', ':woman_getting_haircut_dark_skin_tone:': '💇ðŸ¿â€â™€ï¸', ':woman_getting_haircut_tone5:': '💇ðŸ¿â€â™€ï¸', ':woman_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»â€â™€ï¸', ':woman_golfing_tone1:': 'ðŸŒï¸ðŸ»â€â™€ï¸', ':woman_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼â€â™€ï¸', ':woman_golfing_tone2:': 'ðŸŒï¸ðŸ¼â€â™€ï¸', ':woman_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½â€â™€ï¸', ':woman_golfing_tone3:': 'ðŸŒï¸ðŸ½â€â™€ï¸', ':woman_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾â€â™€ï¸', ':woman_golfing_tone4:': 'ðŸŒï¸ðŸ¾â€â™€ï¸', ':woman_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿â€â™€ï¸', ':woman_golfing_tone5:': 'ðŸŒï¸ðŸ¿â€â™€ï¸', ':woman_guard_light_skin_tone:': '💂ðŸ»â€â™€ï¸', ':woman_guard_tone1:': '💂ðŸ»â€â™€ï¸', ':woman_guard_medium_light_skin_tone:': '💂ðŸ¼â€â™€ï¸', ':woman_guard_tone2:': '💂ðŸ¼â€â™€ï¸', ':woman_guard_medium_skin_tone:': '💂ðŸ½â€â™€ï¸', ':woman_guard_tone3:': '💂ðŸ½â€â™€ï¸', ':woman_guard_medium_dark_skin_tone:': '💂ðŸ¾â€â™€ï¸', ':woman_guard_tone4:': '💂ðŸ¾â€â™€ï¸', ':woman_guard_dark_skin_tone:': '💂ðŸ¿â€â™€ï¸', ':woman_guard_tone5:': '💂ðŸ¿â€â™€ï¸', ':woman_health_worker_light_skin_tone:': '👩ðŸ»â€âš•ï¸', ':woman_health_worker_tone1:': '👩ðŸ»â€âš•ï¸', ':woman_health_worker_medium_light_skin_tone:': '👩ðŸ¼â€âš•ï¸', ':woman_health_worker_tone2:': '👩ðŸ¼â€âš•ï¸', ':woman_health_worker_medium_skin_tone:': '👩ðŸ½â€âš•ï¸', ':woman_health_worker_tone3:': '👩ðŸ½â€âš•ï¸', ':woman_health_worker_medium_dark_skin_tone:': '👩ðŸ¾â€âš•ï¸', ':woman_health_worker_tone4:': '👩ðŸ¾â€âš•ï¸', ':woman_health_worker_dark_skin_tone:': '👩ðŸ¿â€âš•ï¸', ':woman_health_worker_tone5:': '👩ðŸ¿â€âš•ï¸', ':woman_in_lotus_position_light_skin_tone:': '🧘ðŸ»â€â™€ï¸', ':woman_in_lotus_position_tone1:': '🧘ðŸ»â€â™€ï¸', ':woman_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼â€â™€ï¸', ':woman_in_lotus_position_tone2:': '🧘ðŸ¼â€â™€ï¸', ':woman_in_lotus_position_medium_skin_tone:': '🧘ðŸ½â€â™€ï¸', ':woman_in_lotus_position_tone3:': '🧘ðŸ½â€â™€ï¸', ':woman_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾â€â™€ï¸', ':woman_in_lotus_position_tone4:': '🧘ðŸ¾â€â™€ï¸', ':woman_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿â€â™€ï¸', ':woman_in_lotus_position_tone5:': '🧘ðŸ¿â€â™€ï¸', ':woman_in_steamy_room_light_skin_tone:': '🧖ðŸ»â€â™€ï¸', ':woman_in_steamy_room_tone1:': '🧖ðŸ»â€â™€ï¸', ':woman_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼â€â™€ï¸', ':woman_in_steamy_room_tone2:': '🧖ðŸ¼â€â™€ï¸', ':woman_in_steamy_room_medium_skin_tone:': '🧖ðŸ½â€â™€ï¸', ':woman_in_steamy_room_tone3:': '🧖ðŸ½â€â™€ï¸', ':woman_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾â€â™€ï¸', ':woman_in_steamy_room_tone4:': '🧖ðŸ¾â€â™€ï¸', ':woman_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿â€â™€ï¸', ':woman_in_steamy_room_tone5:': '🧖ðŸ¿â€â™€ï¸', ':woman_judge_light_skin_tone:': '👩ðŸ»â€âš–ï¸', ':woman_judge_tone1:': '👩ðŸ»â€âš–ï¸', ':woman_judge_medium_light_skin_tone:': '👩ðŸ¼â€âš–ï¸', ':woman_judge_tone2:': '👩ðŸ¼â€âš–ï¸', ':woman_judge_medium_skin_tone:': '👩ðŸ½â€âš–ï¸', ':woman_judge_tone3:': '👩ðŸ½â€âš–ï¸', ':woman_judge_medium_dark_skin_tone:': '👩ðŸ¾â€âš–ï¸', ':woman_judge_tone4:': '👩ðŸ¾â€âš–ï¸', ':woman_judge_dark_skin_tone:': '👩ðŸ¿â€âš–ï¸', ':woman_judge_tone5:': '👩ðŸ¿â€âš–ï¸', ':woman_juggling_light_skin_tone:': '🤹ðŸ»â€â™€ï¸', ':woman_juggling_tone1:': '🤹ðŸ»â€â™€ï¸', ':woman_juggling_medium_light_skin_tone:': '🤹ðŸ¼â€â™€ï¸', ':woman_juggling_tone2:': '🤹ðŸ¼â€â™€ï¸', ':woman_juggling_medium_skin_tone:': '🤹ðŸ½â€â™€ï¸', ':woman_juggling_tone3:': '🤹ðŸ½â€â™€ï¸', ':woman_juggling_medium_dark_skin_tone:': '🤹ðŸ¾â€â™€ï¸', ':woman_juggling_tone4:': '🤹ðŸ¾â€â™€ï¸', ':woman_juggling_dark_skin_tone:': '🤹ðŸ¿â€â™€ï¸', ':woman_juggling_tone5:': '🤹ðŸ¿â€â™€ï¸', ':woman_kneeling_light_skin_tone:': '🧎ðŸ»â€â™€ï¸', ':woman_kneeling_tone1:': '🧎ðŸ»â€â™€ï¸', ':woman_kneeling_medium_light_skin_tone:': '🧎ðŸ¼â€â™€ï¸', ':woman_kneeling_tone2:': '🧎ðŸ¼â€â™€ï¸', ':woman_kneeling_medium_skin_tone:': '🧎ðŸ½â€â™€ï¸', ':woman_kneeling_tone3:': '🧎ðŸ½â€â™€ï¸', ':woman_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾â€â™€ï¸', ':woman_kneeling_tone4:': '🧎ðŸ¾â€â™€ï¸', ':woman_kneeling_dark_skin_tone:': '🧎ðŸ¿â€â™€ï¸', ':woman_kneeling_tone5:': '🧎ðŸ¿â€â™€ï¸', ':woman_lifting_weights_light_skin_tone:': 'ðŸ‹ï¸ðŸ»â€â™€ï¸', ':woman_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»â€â™€ï¸', ':woman_lifting_weights_medium_light_skin_tone:': 'ðŸ‹ï¸ðŸ¼â€â™€ï¸', ':woman_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼â€â™€ï¸', ':woman_lifting_weights_medium_skin_tone:': 'ðŸ‹ï¸ðŸ½â€â™€ï¸', ':woman_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½â€â™€ï¸', ':woman_lifting_weights_medium_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¾â€â™€ï¸', ':woman_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾â€â™€ï¸', ':woman_lifting_weights_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¿â€â™€ï¸', ':woman_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿â€â™€ï¸', ':woman_mage_light_skin_tone:': '🧙ðŸ»â€â™€ï¸', ':woman_mage_tone1:': '🧙ðŸ»â€â™€ï¸', ':woman_mage_medium_light_skin_tone:': '🧙ðŸ¼â€â™€ï¸', ':woman_mage_tone2:': '🧙ðŸ¼â€â™€ï¸', ':woman_mage_medium_skin_tone:': '🧙ðŸ½â€â™€ï¸', ':woman_mage_tone3:': '🧙ðŸ½â€â™€ï¸', ':woman_mage_medium_dark_skin_tone:': '🧙ðŸ¾â€â™€ï¸', ':woman_mage_tone4:': '🧙ðŸ¾â€â™€ï¸', ':woman_mage_dark_skin_tone:': '🧙ðŸ¿â€â™€ï¸', ':woman_mage_tone5:': '🧙ðŸ¿â€â™€ï¸', ':woman_mountain_biking_light_skin_tone:': '🚵ðŸ»â€â™€ï¸', ':woman_mountain_biking_tone1:': '🚵ðŸ»â€â™€ï¸', ':woman_mountain_biking_medium_light_skin_tone:': '🚵ðŸ¼â€â™€ï¸', ':woman_mountain_biking_tone2:': '🚵ðŸ¼â€â™€ï¸', ':woman_mountain_biking_medium_skin_tone:': '🚵ðŸ½â€â™€ï¸', ':woman_mountain_biking_tone3:': '🚵ðŸ½â€â™€ï¸', ':woman_mountain_biking_medium_dark_skin_tone:': '🚵ðŸ¾â€â™€ï¸', ':woman_mountain_biking_tone4:': '🚵ðŸ¾â€â™€ï¸', ':woman_mountain_biking_dark_skin_tone:': '🚵ðŸ¿â€â™€ï¸', ':woman_mountain_biking_tone5:': '🚵ðŸ¿â€â™€ï¸', ':woman_pilot_light_skin_tone:': '👩ðŸ»â€âœˆï¸', ':woman_pilot_tone1:': '👩ðŸ»â€âœˆï¸', ':woman_pilot_medium_light_skin_tone:': '👩ðŸ¼â€âœˆï¸', ':woman_pilot_tone2:': '👩ðŸ¼â€âœˆï¸', ':woman_pilot_medium_skin_tone:': '👩ðŸ½â€âœˆï¸', ':woman_pilot_tone3:': '👩ðŸ½â€âœˆï¸', ':woman_pilot_medium_dark_skin_tone:': '👩ðŸ¾â€âœˆï¸', ':woman_pilot_tone4:': '👩ðŸ¾â€âœˆï¸', ':woman_pilot_dark_skin_tone:': '👩ðŸ¿â€âœˆï¸', ':woman_pilot_tone5:': '👩ðŸ¿â€âœˆï¸', ':woman_playing_handball_light_skin_tone:': '🤾ðŸ»â€â™€ï¸', ':woman_playing_handball_tone1:': '🤾ðŸ»â€â™€ï¸', ':woman_playing_handball_medium_light_skin_tone:': '🤾ðŸ¼â€â™€ï¸', ':woman_playing_handball_tone2:': '🤾ðŸ¼â€â™€ï¸', ':woman_playing_handball_medium_skin_tone:': '🤾ðŸ½â€â™€ï¸', ':woman_playing_handball_tone3:': '🤾ðŸ½â€â™€ï¸', ':woman_playing_handball_medium_dark_skin_tone:': '🤾ðŸ¾â€â™€ï¸', ':woman_playing_handball_tone4:': '🤾ðŸ¾â€â™€ï¸', ':woman_playing_handball_dark_skin_tone:': '🤾ðŸ¿â€â™€ï¸', ':woman_playing_handball_tone5:': '🤾ðŸ¿â€â™€ï¸', ':woman_playing_water_polo_light_skin_tone:': '🤽ðŸ»â€â™€ï¸', ':woman_playing_water_polo_tone1:': '🤽ðŸ»â€â™€ï¸', ':woman_playing_water_polo_medium_light_skin_tone:': '🤽ðŸ¼â€â™€ï¸', ':woman_playing_water_polo_tone2:': '🤽ðŸ¼â€â™€ï¸', ':woman_playing_water_polo_medium_skin_tone:': '🤽ðŸ½â€â™€ï¸', ':woman_playing_water_polo_tone3:': '🤽ðŸ½â€â™€ï¸', ':woman_playing_water_polo_medium_dark_skin_tone:': '🤽ðŸ¾â€â™€ï¸', ':woman_playing_water_polo_tone4:': '🤽ðŸ¾â€â™€ï¸', ':woman_playing_water_polo_dark_skin_tone:': '🤽ðŸ¿â€â™€ï¸', ':woman_playing_water_polo_tone5:': '🤽ðŸ¿â€â™€ï¸', ':woman_police_officer_light_skin_tone:': '👮ðŸ»â€â™€ï¸', ':woman_police_officer_tone1:': '👮ðŸ»â€â™€ï¸', ':woman_police_officer_medium_light_skin_tone:': '👮ðŸ¼â€â™€ï¸', ':woman_police_officer_tone2:': '👮ðŸ¼â€â™€ï¸', ':woman_police_officer_medium_skin_tone:': '👮ðŸ½â€â™€ï¸', ':woman_police_officer_tone3:': '👮ðŸ½â€â™€ï¸', ':woman_police_officer_medium_dark_skin_tone:': '👮ðŸ¾â€â™€ï¸', ':woman_police_officer_tone4:': '👮ðŸ¾â€â™€ï¸', ':woman_police_officer_dark_skin_tone:': '👮ðŸ¿â€â™€ï¸', ':woman_police_officer_tone5:': '👮ðŸ¿â€â™€ï¸', ':woman_pouting_light_skin_tone:': '🙎ðŸ»â€â™€ï¸', ':woman_pouting_tone1:': '🙎ðŸ»â€â™€ï¸', ':woman_pouting_medium_light_skin_tone:': '🙎ðŸ¼â€â™€ï¸', ':woman_pouting_tone2:': '🙎ðŸ¼â€â™€ï¸', ':woman_pouting_medium_skin_tone:': '🙎ðŸ½â€â™€ï¸', ':woman_pouting_tone3:': '🙎ðŸ½â€â™€ï¸', ':woman_pouting_medium_dark_skin_tone:': '🙎ðŸ¾â€â™€ï¸', ':woman_pouting_tone4:': '🙎ðŸ¾â€â™€ï¸', ':woman_pouting_dark_skin_tone:': '🙎ðŸ¿â€â™€ï¸', ':woman_pouting_tone5:': '🙎ðŸ¿â€â™€ï¸', ':woman_raising_hand_light_skin_tone:': '🙋ðŸ»â€â™€ï¸', ':woman_raising_hand_tone1:': '🙋ðŸ»â€â™€ï¸', ':woman_raising_hand_medium_light_skin_tone:': '🙋ðŸ¼â€â™€ï¸', ':woman_raising_hand_tone2:': '🙋ðŸ¼â€â™€ï¸', ':woman_raising_hand_medium_skin_tone:': '🙋ðŸ½â€â™€ï¸', ':woman_raising_hand_tone3:': '🙋ðŸ½â€â™€ï¸', ':woman_raising_hand_medium_dark_skin_tone:': '🙋ðŸ¾â€â™€ï¸', ':woman_raising_hand_tone4:': '🙋ðŸ¾â€â™€ï¸', ':woman_raising_hand_dark_skin_tone:': '🙋ðŸ¿â€â™€ï¸', ':woman_raising_hand_tone5:': '🙋ðŸ¿â€â™€ï¸', ':woman_rowing_boat_light_skin_tone:': '🚣ðŸ»â€â™€ï¸', ':woman_rowing_boat_tone1:': '🚣ðŸ»â€â™€ï¸', ':woman_rowing_boat_medium_light_skin_tone:': '🚣ðŸ¼â€â™€ï¸', ':woman_rowing_boat_tone2:': '🚣ðŸ¼â€â™€ï¸', ':woman_rowing_boat_medium_skin_tone:': '🚣ðŸ½â€â™€ï¸', ':woman_rowing_boat_tone3:': '🚣ðŸ½â€â™€ï¸', ':woman_rowing_boat_medium_dark_skin_tone:': '🚣ðŸ¾â€â™€ï¸', ':woman_rowing_boat_tone4:': '🚣ðŸ¾â€â™€ï¸', ':woman_rowing_boat_dark_skin_tone:': '🚣ðŸ¿â€â™€ï¸', ':woman_rowing_boat_tone5:': '🚣ðŸ¿â€â™€ï¸', ':woman_running_light_skin_tone:': 'ðŸƒðŸ»â€â™€ï¸', ':woman_running_tone1:': 'ðŸƒðŸ»â€â™€ï¸', ':woman_running_medium_light_skin_tone:': 'ðŸƒðŸ¼â€â™€ï¸', ':woman_running_tone2:': 'ðŸƒðŸ¼â€â™€ï¸', ':woman_running_medium_skin_tone:': 'ðŸƒðŸ½â€â™€ï¸', ':woman_running_tone3:': 'ðŸƒðŸ½â€â™€ï¸', ':woman_running_medium_dark_skin_tone:': 'ðŸƒðŸ¾â€â™€ï¸', ':woman_running_tone4:': 'ðŸƒðŸ¾â€â™€ï¸', ':woman_running_dark_skin_tone:': 'ðŸƒðŸ¿â€â™€ï¸', ':woman_running_tone5:': 'ðŸƒðŸ¿â€â™€ï¸', ':woman_shrugging_light_skin_tone:': '🤷ðŸ»â€â™€ï¸', ':woman_shrugging_tone1:': '🤷ðŸ»â€â™€ï¸', ':woman_shrugging_medium_light_skin_tone:': '🤷ðŸ¼â€â™€ï¸', ':woman_shrugging_tone2:': '🤷ðŸ¼â€â™€ï¸', ':woman_shrugging_medium_skin_tone:': '🤷ðŸ½â€â™€ï¸', ':woman_shrugging_tone3:': '🤷ðŸ½â€â™€ï¸', ':woman_shrugging_medium_dark_skin_tone:': '🤷ðŸ¾â€â™€ï¸', ':woman_shrugging_tone4:': '🤷ðŸ¾â€â™€ï¸', ':woman_shrugging_dark_skin_tone:': '🤷ðŸ¿â€â™€ï¸', ':woman_shrugging_tone5:': '🤷ðŸ¿â€â™€ï¸', ':woman_standing_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', ':woman_standing_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', ':woman_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', ':woman_standing_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', ':woman_standing_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', ':woman_standing_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', ':woman_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', ':woman_standing_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', ':woman_standing_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', ':woman_standing_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', ':woman_superhero_light_skin_tone:': '🦸ðŸ»â€â™€ï¸', ':woman_superhero_tone1:': '🦸ðŸ»â€â™€ï¸', ':woman_superhero_medium_light_skin_tone:': '🦸ðŸ¼â€â™€ï¸', ':woman_superhero_tone2:': '🦸ðŸ¼â€â™€ï¸', ':woman_superhero_medium_skin_tone:': '🦸ðŸ½â€â™€ï¸', ':woman_superhero_tone3:': '🦸ðŸ½â€â™€ï¸', ':woman_superhero_medium_dark_skin_tone:': '🦸ðŸ¾â€â™€ï¸', ':woman_superhero_tone4:': '🦸ðŸ¾â€â™€ï¸', ':woman_superhero_dark_skin_tone:': '🦸ðŸ¿â€â™€ï¸', ':woman_superhero_tone5:': '🦸ðŸ¿â€â™€ï¸', ':woman_supervillain_light_skin_tone:': '🦹ðŸ»â€â™€ï¸', ':woman_supervillain_tone1:': '🦹ðŸ»â€â™€ï¸', ':woman_supervillain_medium_light_skin_tone:': '🦹ðŸ¼â€â™€ï¸', ':woman_supervillain_tone2:': '🦹ðŸ¼â€â™€ï¸', ':woman_supervillain_medium_skin_tone:': '🦹ðŸ½â€â™€ï¸', ':woman_supervillain_tone3:': '🦹ðŸ½â€â™€ï¸', ':woman_supervillain_medium_dark_skin_tone:': '🦹ðŸ¾â€â™€ï¸', ':woman_supervillain_tone4:': '🦹ðŸ¾â€â™€ï¸', ':woman_supervillain_dark_skin_tone:': '🦹ðŸ¿â€â™€ï¸', ':woman_supervillain_tone5:': '🦹ðŸ¿â€â™€ï¸', ':woman_surfing_light_skin_tone:': 'ðŸ„ðŸ»â€â™€ï¸', ':woman_surfing_tone1:': 'ðŸ„ðŸ»â€â™€ï¸', ':woman_surfing_medium_light_skin_tone:': 'ðŸ„ðŸ¼â€â™€ï¸', ':woman_surfing_tone2:': 'ðŸ„ðŸ¼â€â™€ï¸', ':woman_surfing_medium_skin_tone:': 'ðŸ„ðŸ½â€â™€ï¸', ':woman_surfing_tone3:': 'ðŸ„ðŸ½â€â™€ï¸', ':woman_surfing_medium_dark_skin_tone:': 'ðŸ„ðŸ¾â€â™€ï¸', ':woman_surfing_tone4:': 'ðŸ„ðŸ¾â€â™€ï¸', ':woman_surfing_dark_skin_tone:': 'ðŸ„ðŸ¿â€â™€ï¸', ':woman_surfing_tone5:': 'ðŸ„ðŸ¿â€â™€ï¸', ':woman_swimming_light_skin_tone:': 'ðŸŠðŸ»â€â™€ï¸', ':woman_swimming_tone1:': 'ðŸŠðŸ»â€â™€ï¸', ':woman_swimming_medium_light_skin_tone:': 'ðŸŠðŸ¼â€â™€ï¸', ':woman_swimming_tone2:': 'ðŸŠðŸ¼â€â™€ï¸', ':woman_swimming_medium_skin_tone:': 'ðŸŠðŸ½â€â™€ï¸', ':woman_swimming_tone3:': 'ðŸŠðŸ½â€â™€ï¸', ':woman_swimming_medium_dark_skin_tone:': 'ðŸŠðŸ¾â€â™€ï¸', ':woman_swimming_tone4:': 'ðŸŠðŸ¾â€â™€ï¸', ':woman_swimming_dark_skin_tone:': 'ðŸŠðŸ¿â€â™€ï¸', ':woman_swimming_tone5:': 'ðŸŠðŸ¿â€â™€ï¸', ':woman_tipping_hand_light_skin_tone:': 'ðŸ’ðŸ»â€â™€ï¸', ':woman_tipping_hand_tone1:': 'ðŸ’ðŸ»â€â™€ï¸', ':woman_tipping_hand_medium_light_skin_tone:': 'ðŸ’ðŸ¼â€â™€ï¸', ':woman_tipping_hand_tone2:': 'ðŸ’ðŸ¼â€â™€ï¸', ':woman_tipping_hand_medium_skin_tone:': 'ðŸ’ðŸ½â€â™€ï¸', ':woman_tipping_hand_tone3:': 'ðŸ’ðŸ½â€â™€ï¸', ':woman_tipping_hand_medium_dark_skin_tone:': 'ðŸ’ðŸ¾â€â™€ï¸', ':woman_tipping_hand_tone4:': 'ðŸ’ðŸ¾â€â™€ï¸', ':woman_tipping_hand_dark_skin_tone:': 'ðŸ’ðŸ¿â€â™€ï¸', ':woman_tipping_hand_tone5:': 'ðŸ’ðŸ¿â€â™€ï¸', ':woman_vampire_light_skin_tone:': '🧛ðŸ»â€â™€ï¸', ':woman_vampire_tone1:': '🧛ðŸ»â€â™€ï¸', ':woman_vampire_medium_light_skin_tone:': '🧛ðŸ¼â€â™€ï¸', ':woman_vampire_tone2:': '🧛ðŸ¼â€â™€ï¸', ':woman_vampire_medium_skin_tone:': '🧛ðŸ½â€â™€ï¸', ':woman_vampire_tone3:': '🧛ðŸ½â€â™€ï¸', ':woman_vampire_medium_dark_skin_tone:': '🧛ðŸ¾â€â™€ï¸', ':woman_vampire_tone4:': '🧛ðŸ¾â€â™€ï¸', ':woman_vampire_dark_skin_tone:': '🧛ðŸ¿â€â™€ï¸', ':woman_vampire_tone5:': '🧛ðŸ¿â€â™€ï¸', ':woman_walking_light_skin_tone:': '🚶ðŸ»â€â™€ï¸', ':woman_walking_tone1:': '🚶ðŸ»â€â™€ï¸', ':woman_walking_medium_light_skin_tone:': '🚶ðŸ¼â€â™€ï¸', ':woman_walking_tone2:': '🚶ðŸ¼â€â™€ï¸', ':woman_walking_medium_skin_tone:': '🚶ðŸ½â€â™€ï¸', ':woman_walking_tone3:': '🚶ðŸ½â€â™€ï¸', ':woman_walking_medium_dark_skin_tone:': '🚶ðŸ¾â€â™€ï¸', ':woman_walking_tone4:': '🚶ðŸ¾â€â™€ï¸', ':woman_walking_dark_skin_tone:': '🚶ðŸ¿â€â™€ï¸', ':woman_walking_tone5:': '🚶ðŸ¿â€â™€ï¸', ':woman_wearing_turban_light_skin_tone:': '👳ðŸ»â€â™€ï¸', ':woman_wearing_turban_tone1:': '👳ðŸ»â€â™€ï¸', ':woman_wearing_turban_medium_light_skin_tone:': '👳ðŸ¼â€â™€ï¸', ':woman_wearing_turban_tone2:': '👳ðŸ¼â€â™€ï¸', ':woman_wearing_turban_medium_skin_tone:': '👳ðŸ½â€â™€ï¸', ':woman_wearing_turban_tone3:': '👳ðŸ½â€â™€ï¸', ':woman_wearing_turban_medium_dark_skin_tone:': '👳ðŸ¾â€â™€ï¸', ':woman_wearing_turban_tone4:': '👳ðŸ¾â€â™€ï¸', ':woman_wearing_turban_dark_skin_tone:': '👳ðŸ¿â€â™€ï¸', ':woman_wearing_turban_tone5:': '👳ðŸ¿â€â™€ï¸', ':man_bouncing_ball_light_skin_tone:': '⛹ï¸ðŸ»â€â™‚ï¸', ':man_bouncing_ball_tone1:': '⛹ï¸ðŸ»â€â™‚ï¸', ':man_bouncing_ball_medium_light_skin_tone:': '⛹ï¸ðŸ¼â€â™‚ï¸', ':man_bouncing_ball_tone2:': '⛹ï¸ðŸ¼â€â™‚ï¸', ':man_bouncing_ball_medium_skin_tone:': '⛹ï¸ðŸ½â€â™‚ï¸', ':man_bouncing_ball_tone3:': '⛹ï¸ðŸ½â€â™‚ï¸', ':man_bouncing_ball_medium_dark_skin_tone:': '⛹ï¸ðŸ¾â€â™‚ï¸', ':man_bouncing_ball_tone4:': '⛹ï¸ðŸ¾â€â™‚ï¸', ':man_bouncing_ball_dark_skin_tone:': '⛹ï¸ðŸ¿â€â™‚ï¸', ':man_bouncing_ball_tone5:': '⛹ï¸ðŸ¿â€â™‚ï¸', ':woman_bouncing_ball_light_skin_tone:': '⛹ï¸ðŸ»â€â™€ï¸', ':woman_bouncing_ball_tone1:': '⛹ï¸ðŸ»â€â™€ï¸', ':woman_bouncing_ball_medium_light_skin_tone:': '⛹ï¸ðŸ¼â€â™€ï¸', ':woman_bouncing_ball_tone2:': '⛹ï¸ðŸ¼â€â™€ï¸', ':woman_bouncing_ball_medium_skin_tone:': '⛹ï¸ðŸ½â€â™€ï¸', ':woman_bouncing_ball_tone3:': '⛹ï¸ðŸ½â€â™€ï¸', ':woman_bouncing_ball_medium_dark_skin_tone:': '⛹ï¸ðŸ¾â€â™€ï¸', ':woman_bouncing_ball_tone4:': '⛹ï¸ðŸ¾â€â™€ï¸', ':woman_bouncing_ball_dark_skin_tone:': '⛹ï¸ðŸ¿â€â™€ï¸', ':woman_bouncing_ball_tone5:': '⛹ï¸ðŸ¿â€â™€ï¸', ':adult_light_skin_tone:': '🧑ðŸ»', ':adult_tone1:': '🧑ðŸ»', ':adult_medium_light_skin_tone:': '🧑ðŸ¼', ':adult_tone2:': '🧑ðŸ¼', ':adult_medium_skin_tone:': '🧑ðŸ½', ':adult_tone3:': '🧑ðŸ½', ':adult_medium_dark_skin_tone:': '🧑ðŸ¾', ':adult_tone4:': '🧑ðŸ¾', ':adult_dark_skin_tone:': '🧑ðŸ¿', ':adult_tone5:': '🧑ðŸ¿', ':angel_tone1:': '👼ðŸ»', ':angel_tone2:': '👼ðŸ¼', ':angel_tone3:': '👼ðŸ½', ':angel_tone4:': '👼ðŸ¾', ':angel_tone5:': '👼ðŸ¿', ':baby_tone1:': '👶ðŸ»', ':baby_tone2:': '👶ðŸ¼', ':baby_tone3:': '👶ðŸ½', ':baby_tone4:': '👶ðŸ¾', ':baby_tone5:': '👶ðŸ¿', ':bath_tone1:': '🛀ðŸ»', ':bath_tone2:': '🛀ðŸ¼', ':bath_tone3:': '🛀ðŸ½', ':bath_tone4:': '🛀ðŸ¾', ':bath_tone5:': '🛀ðŸ¿', ':bearded_person_light_skin_tone:': '🧔ðŸ»', ':bearded_person_tone1:': '🧔ðŸ»', ':bearded_person_medium_light_skin_tone:': '🧔ðŸ¼', ':bearded_person_tone2:': '🧔ðŸ¼', ':bearded_person_medium_skin_tone:': '🧔ðŸ½', ':bearded_person_tone3:': '🧔ðŸ½', ':bearded_person_medium_dark_skin_tone:': '🧔ðŸ¾', ':bearded_person_tone4:': '🧔ðŸ¾', ':bearded_person_dark_skin_tone:': '🧔ðŸ¿', ':bearded_person_tone5:': '🧔ðŸ¿', ':person_with_blond_hair_tone1:': '👱ðŸ»', ':blond_haired_person_tone1:': '👱ðŸ»', ':person_with_blond_hair_tone2:': '👱ðŸ¼', ':blond_haired_person_tone2:': '👱ðŸ¼', ':person_with_blond_hair_tone3:': '👱ðŸ½', ':blond_haired_person_tone3:': '👱ðŸ½', ':person_with_blond_hair_tone4:': '👱ðŸ¾', ':blond_haired_person_tone4:': '👱ðŸ¾', ':person_with_blond_hair_tone5:': '👱ðŸ¿', ':blond_haired_person_tone5:': '👱ðŸ¿', ':boy_tone1:': '👦ðŸ»', ':boy_tone2:': '👦ðŸ¼', ':boy_tone3:': '👦ðŸ½', ':boy_tone4:': '👦ðŸ¾', ':boy_tone5:': '👦ðŸ¿', ':breast_feeding_light_skin_tone:': '🤱ðŸ»', ':breast_feeding_tone1:': '🤱ðŸ»', ':breast_feeding_medium_light_skin_tone:': '🤱ðŸ¼', ':breast_feeding_tone2:': '🤱ðŸ¼', ':breast_feeding_medium_skin_tone:': '🤱ðŸ½', ':breast_feeding_tone3:': '🤱ðŸ½', ':breast_feeding_medium_dark_skin_tone:': '🤱ðŸ¾', ':breast_feeding_tone4:': '🤱ðŸ¾', ':breast_feeding_dark_skin_tone:': '🤱ðŸ¿', ':breast_feeding_tone5:': '🤱ðŸ¿', ':bride_with_veil_tone1:': '👰ðŸ»', ':bride_with_veil_tone2:': '👰ðŸ¼', ':bride_with_veil_tone3:': '👰ðŸ½', ':bride_with_veil_tone4:': '👰ðŸ¾', ':bride_with_veil_tone5:': '👰ðŸ¿', ':call_me_hand_tone1:': '🤙ðŸ»', ':call_me_tone1:': '🤙ðŸ»', ':call_me_hand_tone2:': '🤙ðŸ¼', ':call_me_tone2:': '🤙ðŸ¼', ':call_me_hand_tone3:': '🤙ðŸ½', ':call_me_tone3:': '🤙ðŸ½', ':call_me_hand_tone4:': '🤙ðŸ¾', ':call_me_tone4:': '🤙ðŸ¾', ':call_me_hand_tone5:': '🤙ðŸ¿', ':call_me_tone5:': '🤙ðŸ¿', ':child_light_skin_tone:': '🧒ðŸ»', ':child_tone1:': '🧒ðŸ»', ':child_medium_light_skin_tone:': '🧒ðŸ¼', ':child_tone2:': '🧒ðŸ¼', ':child_medium_skin_tone:': '🧒ðŸ½', ':child_tone3:': '🧒ðŸ½', ':child_medium_dark_skin_tone:': '🧒ðŸ¾', ':child_tone4:': '🧒ðŸ¾', ':child_dark_skin_tone:': '🧒ðŸ¿', ':child_tone5:': '🧒ðŸ¿', ':clap_tone1:': 'ðŸ‘ðŸ»', ':clap_tone2:': 'ðŸ‘ðŸ¼', ':clap_tone3:': 'ðŸ‘ðŸ½', ':clap_tone4:': 'ðŸ‘ðŸ¾', ':clap_tone5:': 'ðŸ‘ðŸ¿', ':construction_worker_tone1:': '👷ðŸ»', ':construction_worker_tone2:': '👷ðŸ¼', ':construction_worker_tone3:': '👷ðŸ½', ':construction_worker_tone4:': '👷ðŸ¾', ':construction_worker_tone5:': '👷ðŸ¿', ':dancer_tone1:': '💃ðŸ»', ':dancer_tone2:': '💃ðŸ¼', ':dancer_tone3:': '💃ðŸ½', ':dancer_tone4:': '💃ðŸ¾', ':dancer_tone5:': '💃ðŸ¿', ':deaf_person_light_skin_tone:': 'ðŸ§ðŸ»', ':deaf_person_tone1:': 'ðŸ§ðŸ»', ':deaf_person_medium_light_skin_tone:': 'ðŸ§ðŸ¼', ':deaf_person_tone2:': 'ðŸ§ðŸ¼', ':deaf_person_medium_skin_tone:': 'ðŸ§ðŸ½', ':deaf_person_tone3:': 'ðŸ§ðŸ½', ':deaf_person_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', ':deaf_person_tone4:': 'ðŸ§ðŸ¾', ':deaf_person_dark_skin_tone:': 'ðŸ§ðŸ¿', ':deaf_person_tone5:': 'ðŸ§ðŸ¿', ':spy_tone1:': '🕵ï¸ðŸ»', ':sleuth_or_spy_tone1:': '🕵ï¸ðŸ»', ':detective_tone1:': '🕵ï¸ðŸ»', ':spy_tone2:': '🕵ï¸ðŸ¼', ':sleuth_or_spy_tone2:': '🕵ï¸ðŸ¼', ':detective_tone2:': '🕵ï¸ðŸ¼', ':spy_tone3:': '🕵ï¸ðŸ½', ':sleuth_or_spy_tone3:': '🕵ï¸ðŸ½', ':detective_tone3:': '🕵ï¸ðŸ½', ':spy_tone4:': '🕵ï¸ðŸ¾', ':sleuth_or_spy_tone4:': '🕵ï¸ðŸ¾', ':detective_tone4:': '🕵ï¸ðŸ¾', ':spy_tone5:': '🕵ï¸ðŸ¿', ':sleuth_or_spy_tone5:': '🕵ï¸ðŸ¿', ':detective_tone5:': '🕵ï¸ðŸ¿', ':ear_tone1:': '👂ðŸ»', ':ear_tone2:': '👂ðŸ¼', ':ear_tone3:': '👂ðŸ½', ':ear_tone4:': '👂ðŸ¾', ':ear_tone5:': '👂ðŸ¿', ':ear_with_hearing_aid_light_skin_tone:': '🦻ðŸ»', ':ear_with_hearing_aid_tone1:': '🦻ðŸ»', ':ear_with_hearing_aid_medium_light_skin_tone:': '🦻ðŸ¼', ':ear_with_hearing_aid_tone2:': '🦻ðŸ¼', ':ear_with_hearing_aid_medium_skin_tone:': '🦻ðŸ½', ':ear_with_hearing_aid_tone3:': '🦻ðŸ½', ':ear_with_hearing_aid_medium_dark_skin_tone:': '🦻ðŸ¾', ':ear_with_hearing_aid_tone4:': '🦻ðŸ¾', ':ear_with_hearing_aid_dark_skin_tone:': '🦻ðŸ¿', ':ear_with_hearing_aid_tone5:': '🦻ðŸ¿', ':elf_light_skin_tone:': 'ðŸ§ðŸ»', ':elf_tone1:': 'ðŸ§ðŸ»', ':elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼', ':elf_tone2:': 'ðŸ§ðŸ¼', ':elf_medium_skin_tone:': 'ðŸ§ðŸ½', ':elf_tone3:': 'ðŸ§ðŸ½', ':elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', ':elf_tone4:': 'ðŸ§ðŸ¾', ':elf_dark_skin_tone:': 'ðŸ§ðŸ¿', ':elf_tone5:': 'ðŸ§ðŸ¿', ':eye_in_speech_bubble:': 'ðŸ‘ï¸â€ðŸ—¨ï¸', ':fairy_light_skin_tone:': '🧚ðŸ»', ':fairy_tone1:': '🧚ðŸ»', ':fairy_medium_light_skin_tone:': '🧚ðŸ¼', ':fairy_tone2:': '🧚ðŸ¼', ':fairy_medium_skin_tone:': '🧚ðŸ½', ':fairy_tone3:': '🧚ðŸ½', ':fairy_medium_dark_skin_tone:': '🧚ðŸ¾', ':fairy_tone4:': '🧚ðŸ¾', ':fairy_dark_skin_tone:': '🧚ðŸ¿', ':fairy_tone5:': '🧚ðŸ¿', ':family_man_boy:': '👨â€ðŸ‘¦', ':family_man_girl:': '👨â€ðŸ‘§', ':family_woman_boy:': '👩â€ðŸ‘¦', ':family_woman_girl:': '👩â€ðŸ‘§', ':hand_with_index_and_middle_fingers_crossed_tone1:': '🤞ðŸ»', ':fingers_crossed_tone1:': '🤞ðŸ»', ':hand_with_index_and_middle_fingers_crossed_tone2:': '🤞ðŸ¼', ':fingers_crossed_tone2:': '🤞ðŸ¼', ':hand_with_index_and_middle_fingers_crossed_tone3:': '🤞ðŸ½', ':fingers_crossed_tone3:': '🤞ðŸ½', ':hand_with_index_and_middle_fingers_crossed_tone4:': '🤞ðŸ¾', ':fingers_crossed_tone4:': '🤞ðŸ¾', ':hand_with_index_and_middle_fingers_crossed_tone5:': '🤞ðŸ¿', ':fingers_crossed_tone5:': '🤞ðŸ¿', ':ac:': '🇦🇨', ':flag_ac:': '🇦🇨', ':ad:': '🇦🇩', ':flag_ad:': '🇦🇩', ':ae:': '🇦🇪', ':flag_ae:': '🇦🇪', ':af:': '🇦🇫', ':flag_af:': '🇦🇫', ':ag:': '🇦🇬', ':flag_ag:': '🇦🇬', ':ai:': '🇦🇮', ':flag_ai:': '🇦🇮', ':al:': '🇦🇱', ':flag_al:': '🇦🇱', ':am:': '🇦🇲', ':flag_am:': '🇦🇲', ':ao:': '🇦🇴', ':flag_ao:': '🇦🇴', ':aq:': '🇦🇶', ':flag_aq:': '🇦🇶', ':ar:': '🇦🇷', ':flag_ar:': '🇦🇷', ':as:': '🇦🇸', ':flag_as:': '🇦🇸', ':at:': '🇦🇹', ':flag_at:': '🇦🇹', ':au:': '🇦🇺', ':flag_au:': '🇦🇺', ':aw:': '🇦🇼', ':flag_aw:': '🇦🇼', ':ax:': '🇦🇽', ':flag_ax:': '🇦🇽', ':az:': '🇦🇿', ':flag_az:': '🇦🇿', ':ba:': '🇧🇦', ':flag_ba:': '🇧🇦', ':bb:': '🇧🇧', ':flag_bb:': '🇧🇧', ':bd:': '🇧🇩', ':flag_bd:': '🇧🇩', ':be:': '🇧🇪', ':flag_be:': '🇧🇪', ':bf:': '🇧🇫', ':flag_bf:': '🇧🇫', ':bg:': '🇧🇬', ':flag_bg:': '🇧🇬', ':bh:': '🇧ðŸ‡', ':flag_bh:': '🇧ðŸ‡', ':bi:': '🇧🇮', ':flag_bi:': '🇧🇮', ':bj:': '🇧🇯', ':flag_bj:': '🇧🇯', ':bm:': '🇧🇲', ':flag_bm:': '🇧🇲', ':bn:': '🇧🇳', ':flag_bn:': '🇧🇳', ':bo:': '🇧🇴', ':flag_bo:': '🇧🇴', ':br:': '🇧🇷', ':flag_br:': '🇧🇷', ':bs:': '🇧🇸', ':flag_bs:': '🇧🇸', ':bt:': '🇧🇹', ':flag_bt:': '🇧🇹', ':bv:': '🇧🇻', ':flag_bv:': '🇧🇻', ':bw:': '🇧🇼', ':flag_bw:': '🇧🇼', ':by:': '🇧🇾', ':flag_by:': '🇧🇾', ':bz:': '🇧🇿', ':flag_bz:': '🇧🇿', ':ca:': '🇨🇦', ':flag_ca:': '🇨🇦', ':cc:': '🇨🇨', ':flag_cc:': '🇨🇨', ':congo:': '🇨🇩', ':flag_cd:': '🇨🇩', ':cf:': '🇨🇫', ':flag_cf:': '🇨🇫', ':cg:': '🇨🇬', ':flag_cg:': '🇨🇬', ':ch:': '🇨ðŸ‡', ':flag_ch:': '🇨ðŸ‡', ':ci:': '🇨🇮', ':flag_ci:': '🇨🇮', ':ck:': '🇨🇰', ':flag_ck:': '🇨🇰', ':chile:': '🇨🇱', ':flag_cl:': '🇨🇱', ':cm:': '🇨🇲', ':flag_cm:': '🇨🇲', ':cn:': '🇨🇳', ':flag_cn:': '🇨🇳', ':co:': '🇨🇴', ':flag_co:': '🇨🇴', ':cp:': '🇨🇵', ':flag_cp:': '🇨🇵', ':cr:': '🇨🇷', ':flag_cr:': '🇨🇷', ':cu:': '🇨🇺', ':flag_cu:': '🇨🇺', ':cv:': '🇨🇻', ':flag_cv:': '🇨🇻', ':cw:': '🇨🇼', ':flag_cw:': '🇨🇼', ':cx:': '🇨🇽', ':flag_cx:': '🇨🇽', ':cy:': '🇨🇾', ':flag_cy:': '🇨🇾', ':cz:': '🇨🇿', ':flag_cz:': '🇨🇿', ':de:': '🇩🇪', ':flag_de:': '🇩🇪', ':dj:': '🇩🇯', ':flag_dj:': '🇩🇯', ':dk:': '🇩🇰', ':flag_dk:': '🇩🇰', ':dm:': '🇩🇲', ':flag_dm:': '🇩🇲', ':do:': '🇩🇴', ':flag_do:': '🇩🇴', ':dz:': '🇩🇿', ':flag_dz:': '🇩🇿', ':ec:': '🇪🇨', ':flag_ec:': '🇪🇨', ':ee:': '🇪🇪', ':flag_ee:': '🇪🇪', ':eg:': '🇪🇬', ':flag_eg:': '🇪🇬', ':er:': '🇪🇷', ':flag_er:': '🇪🇷', ':es:': '🇪🇸', ':flag_es:': '🇪🇸', ':et:': '🇪🇹', ':flag_et:': '🇪🇹', ':eu:': '🇪🇺', ':flag_eu:': '🇪🇺', ':fi:': '🇫🇮', ':flag_fi:': '🇫🇮', ':fj:': '🇫🇯', ':flag_fj:': '🇫🇯', ':fm:': '🇫🇲', ':flag_fm:': '🇫🇲', ':fo:': '🇫🇴', ':flag_fo:': '🇫🇴', ':fr:': '🇫🇷', ':flag_fr:': '🇫🇷', ':ga:': '🇬🇦', ':flag_ga:': '🇬🇦', ':gb:': '🇬🇧', ':flag_gb:': '🇬🇧', ':gd:': '🇬🇩', ':flag_gd:': '🇬🇩', ':ge:': '🇬🇪', ':flag_ge:': '🇬🇪', ':gg:': '🇬🇬', ':flag_gg:': '🇬🇬', ':gh:': '🇬ðŸ‡', ':flag_gh:': '🇬ðŸ‡', ':gi:': '🇬🇮', ':flag_gi:': '🇬🇮', ':gl:': '🇬🇱', ':flag_gl:': '🇬🇱', ':gm:': '🇬🇲', ':flag_gm:': '🇬🇲', ':gn:': '🇬🇳', ':flag_gn:': '🇬🇳', ':gq:': '🇬🇶', ':flag_gq:': '🇬🇶', ':gr:': '🇬🇷', ':flag_gr:': '🇬🇷', ':gt:': '🇬🇹', ':flag_gt:': '🇬🇹', ':gu:': '🇬🇺', ':flag_gu:': '🇬🇺', ':gw:': '🇬🇼', ':flag_gw:': '🇬🇼', ':gy:': '🇬🇾', ':flag_gy:': '🇬🇾', ':hk:': 'ðŸ‡ðŸ‡°', ':flag_hk:': 'ðŸ‡ðŸ‡°', ':hm:': 'ðŸ‡ðŸ‡²', ':flag_hm:': 'ðŸ‡ðŸ‡²', ':hn:': 'ðŸ‡ðŸ‡³', ':flag_hn:': 'ðŸ‡ðŸ‡³', ':hr:': 'ðŸ‡ðŸ‡·', ':flag_hr:': 'ðŸ‡ðŸ‡·', ':ht:': 'ðŸ‡ðŸ‡¹', ':flag_ht:': 'ðŸ‡ðŸ‡¹', ':hu:': 'ðŸ‡ðŸ‡º', ':flag_hu:': 'ðŸ‡ðŸ‡º', ':ic:': '🇮🇨', ':flag_ic:': '🇮🇨', ':indonesia:': '🇮🇩', ':flag_id:': '🇮🇩', ':ie:': '🇮🇪', ':flag_ie:': '🇮🇪', ':il:': '🇮🇱', ':flag_il:': '🇮🇱', ':im:': '🇮🇲', ':flag_im:': '🇮🇲', ':in:': '🇮🇳', ':flag_in:': '🇮🇳', ':io:': '🇮🇴', ':flag_io:': '🇮🇴', ':iq:': '🇮🇶', ':flag_iq:': '🇮🇶', ':ir:': '🇮🇷', ':flag_ir:': '🇮🇷', ':is:': '🇮🇸', ':flag_is:': '🇮🇸', ':it:': '🇮🇹', ':flag_it:': '🇮🇹', ':je:': '🇯🇪', ':flag_je:': '🇯🇪', ':jm:': '🇯🇲', ':flag_jm:': '🇯🇲', ':jo:': '🇯🇴', ':flag_jo:': '🇯🇴', ':jp:': '🇯🇵', ':flag_jp:': '🇯🇵', ':ke:': '🇰🇪', ':flag_ke:': '🇰🇪', ':kg:': '🇰🇬', ':flag_kg:': '🇰🇬', ':kh:': '🇰ðŸ‡', ':flag_kh:': '🇰ðŸ‡', ':ki:': '🇰🇮', ':flag_ki:': '🇰🇮', ':km:': '🇰🇲', ':flag_km:': '🇰🇲', ':kn:': '🇰🇳', ':flag_kn:': '🇰🇳', ':kp:': '🇰🇵', ':flag_kp:': '🇰🇵', ':kr:': '🇰🇷', ':flag_kr:': '🇰🇷', ':kw:': '🇰🇼', ':flag_kw:': '🇰🇼', ':ky:': '🇰🇾', ':flag_ky:': '🇰🇾', ':kz:': '🇰🇿', ':flag_kz:': '🇰🇿', ':la:': '🇱🇦', ':flag_la:': '🇱🇦', ':lb:': '🇱🇧', ':flag_lb:': '🇱🇧', ':lc:': '🇱🇨', ':flag_lc:': '🇱🇨', ':li:': '🇱🇮', ':flag_li:': '🇱🇮', ':lk:': '🇱🇰', ':flag_lk:': '🇱🇰', ':lr:': '🇱🇷', ':flag_lr:': '🇱🇷', ':ls:': '🇱🇸', ':flag_ls:': '🇱🇸', ':lt:': '🇱🇹', ':flag_lt:': '🇱🇹', ':lu:': '🇱🇺', ':flag_lu:': '🇱🇺', ':lv:': '🇱🇻', ':flag_lv:': '🇱🇻', ':ly:': '🇱🇾', ':flag_ly:': '🇱🇾', ':ma:': '🇲🇦', ':flag_ma:': '🇲🇦', ':mc:': '🇲🇨', ':flag_mc:': '🇲🇨', ':md:': '🇲🇩', ':flag_md:': '🇲🇩', ':me:': '🇲🇪', ':flag_me:': '🇲🇪', ':mg:': '🇲🇬', ':flag_mg:': '🇲🇬', ':mh:': '🇲ðŸ‡', ':flag_mh:': '🇲ðŸ‡', ':mk:': '🇲🇰', ':flag_mk:': '🇲🇰', ':ml:': '🇲🇱', ':flag_ml:': '🇲🇱', ':mm:': '🇲🇲', ':flag_mm:': '🇲🇲', ':mn:': '🇲🇳', ':flag_mn:': '🇲🇳', ':mo:': '🇲🇴', ':flag_mo:': '🇲🇴', ':mp:': '🇲🇵', ':flag_mp:': '🇲🇵', ':mr:': '🇲🇷', ':flag_mr:': '🇲🇷', ':ms:': '🇲🇸', ':flag_ms:': '🇲🇸', ':mt:': '🇲🇹', ':flag_mt:': '🇲🇹', ':mu:': '🇲🇺', ':flag_mu:': '🇲🇺', ':mv:': '🇲🇻', ':flag_mv:': '🇲🇻', ':mw:': '🇲🇼', ':flag_mw:': '🇲🇼', ':mx:': '🇲🇽', ':flag_mx:': '🇲🇽', ':my:': '🇲🇾', ':flag_my:': '🇲🇾', ':mz:': '🇲🇿', ':flag_mz:': '🇲🇿', ':na:': '🇳🇦', ':flag_na:': '🇳🇦', ':ne:': '🇳🇪', ':flag_ne:': '🇳🇪', ':nf:': '🇳🇫', ':flag_nf:': '🇳🇫', ':nigeria:': '🇳🇬', ':flag_ng:': '🇳🇬', ':ni:': '🇳🇮', ':flag_ni:': '🇳🇮', ':nl:': '🇳🇱', ':flag_nl:': '🇳🇱', ':no:': '🇳🇴', ':flag_no:': '🇳🇴', ':np:': '🇳🇵', ':flag_np:': '🇳🇵', ':nr:': '🇳🇷', ':flag_nr:': '🇳🇷', ':nu:': '🇳🇺', ':flag_nu:': '🇳🇺', ':nz:': '🇳🇿', ':flag_nz:': '🇳🇿', ':om:': '🇴🇲', ':flag_om:': '🇴🇲', ':pa:': '🇵🇦', ':flag_pa:': '🇵🇦', ':pe:': '🇵🇪', ':flag_pe:': '🇵🇪', ':pf:': '🇵🇫', ':flag_pf:': '🇵🇫', ':pg:': '🇵🇬', ':flag_pg:': '🇵🇬', ':ph:': '🇵ðŸ‡', ':flag_ph:': '🇵ðŸ‡', ':pk:': '🇵🇰', ':flag_pk:': '🇵🇰', ':pl:': '🇵🇱', ':flag_pl:': '🇵🇱', ':pn:': '🇵🇳', ':flag_pn:': '🇵🇳', ':pr:': '🇵🇷', ':flag_pr:': '🇵🇷', ':ps:': '🇵🇸', ':flag_ps:': '🇵🇸', ':pt:': '🇵🇹', ':flag_pt:': '🇵🇹', ':pw:': '🇵🇼', ':flag_pw:': '🇵🇼', ':py:': '🇵🇾', ':flag_py:': '🇵🇾', ':qa:': '🇶🇦', ':flag_qa:': '🇶🇦', ':ro:': '🇷🇴', ':flag_ro:': '🇷🇴', ':rs:': '🇷🇸', ':flag_rs:': '🇷🇸', ':ru:': '🇷🇺', ':flag_ru:': '🇷🇺', ':rw:': '🇷🇼', ':flag_rw:': '🇷🇼', ':saudiarabia:': '🇸🇦', ':saudi:': '🇸🇦', ':flag_sa:': '🇸🇦', ':sb:': '🇸🇧', ':flag_sb:': '🇸🇧', ':sc:': '🇸🇨', ':flag_sc:': '🇸🇨', ':sd:': '🇸🇩', ':flag_sd:': '🇸🇩', ':se:': '🇸🇪', ':flag_se:': '🇸🇪', ':sg:': '🇸🇬', ':flag_sg:': '🇸🇬', ':sh:': '🇸ðŸ‡', ':flag_sh:': '🇸ðŸ‡', ':si:': '🇸🇮', ':flag_si:': '🇸🇮', ':sj:': '🇸🇯', ':flag_sj:': '🇸🇯', ':sk:': '🇸🇰', ':flag_sk:': '🇸🇰', ':sl:': '🇸🇱', ':flag_sl:': '🇸🇱', ':sm:': '🇸🇲', ':flag_sm:': '🇸🇲', ':sn:': '🇸🇳', ':flag_sn:': '🇸🇳', ':so:': '🇸🇴', ':flag_so:': '🇸🇴', ':sr:': '🇸🇷', ':flag_sr:': '🇸🇷', ':ss:': '🇸🇸', ':flag_ss:': '🇸🇸', ':st:': '🇸🇹', ':flag_st:': '🇸🇹', ':sv:': '🇸🇻', ':flag_sv:': '🇸🇻', ':sx:': '🇸🇽', ':flag_sx:': '🇸🇽', ':sy:': '🇸🇾', ':flag_sy:': '🇸🇾', ':sz:': '🇸🇿', ':flag_sz:': '🇸🇿', ':ta:': '🇹🇦', ':flag_ta:': '🇹🇦', ':tc:': '🇹🇨', ':flag_tc:': '🇹🇨', ':td:': '🇹🇩', ':flag_td:': '🇹🇩', ':tg:': '🇹🇬', ':flag_tg:': '🇹🇬', ':th:': '🇹ðŸ‡', ':flag_th:': '🇹ðŸ‡', ':tj:': '🇹🇯', ':flag_tj:': '🇹🇯', ':tk:': '🇹🇰', ':flag_tk:': '🇹🇰', ':tl:': '🇹🇱', ':flag_tl:': '🇹🇱', ':turkmenistan:': '🇹🇲', ':flag_tm:': '🇹🇲', ':tn:': '🇹🇳', ':flag_tn:': '🇹🇳', ':to:': '🇹🇴', ':flag_to:': '🇹🇴', ':tr:': '🇹🇷', ':flag_tr:': '🇹🇷', ':tt:': '🇹🇹', ':flag_tt:': '🇹🇹', ':tuvalu:': '🇹🇻', ':flag_tv:': '🇹🇻', ':tw:': '🇹🇼', ':flag_tw:': '🇹🇼', ':tz:': '🇹🇿', ':flag_tz:': '🇹🇿', ':ua:': '🇺🇦', ':flag_ua:': '🇺🇦', ':ug:': '🇺🇬', ':flag_ug:': '🇺🇬', ':um:': '🇺🇲', ':flag_um:': '🇺🇲', ':us:': '🇺🇸', ':flag_us:': '🇺🇸', ':uy:': '🇺🇾', ':flag_uy:': '🇺🇾', ':uz:': '🇺🇿', ':flag_uz:': '🇺🇿', ':va:': '🇻🇦', ':flag_va:': '🇻🇦', ':vc:': '🇻🇨', ':flag_vc:': '🇻🇨', ':ve:': '🇻🇪', ':flag_ve:': '🇻🇪', ':vg:': '🇻🇬', ':flag_vg:': '🇻🇬', ':vi:': '🇻🇮', ':flag_vi:': '🇻🇮', ':vn:': '🇻🇳', ':flag_vn:': '🇻🇳', ':vu:': '🇻🇺', ':flag_vu:': '🇻🇺', ':ws:': '🇼🇸', ':flag_ws:': '🇼🇸', ':ye:': '🇾🇪', ':flag_ye:': '🇾🇪', ':za:': '🇿🇦', ':flag_za:': '🇿🇦', ':zm:': '🇿🇲', ':flag_zm:': '🇿🇲', ':zw:': '🇿🇼', ':flag_zw:': '🇿🇼', ':foot_light_skin_tone:': '🦶ðŸ»', ':foot_tone1:': '🦶ðŸ»', ':foot_medium_light_skin_tone:': '🦶ðŸ¼', ':foot_tone2:': '🦶ðŸ¼', ':foot_medium_skin_tone:': '🦶ðŸ½', ':foot_tone3:': '🦶ðŸ½', ':foot_medium_dark_skin_tone:': '🦶ðŸ¾', ':foot_tone4:': '🦶ðŸ¾', ':foot_dark_skin_tone:': '🦶ðŸ¿', ':foot_tone5:': '🦶ðŸ¿', ':girl_tone1:': '👧ðŸ»', ':girl_tone2:': '👧ðŸ¼', ':girl_tone3:': '👧ðŸ½', ':girl_tone4:': '👧ðŸ¾', ':girl_tone5:': '👧ðŸ¿', ':guardsman_tone1:': '💂ðŸ»', ':guard_tone1:': '💂ðŸ»', ':guardsman_tone2:': '💂ðŸ¼', ':guard_tone2:': '💂ðŸ¼', ':guardsman_tone3:': '💂ðŸ½', ':guard_tone3:': '💂ðŸ½', ':guardsman_tone4:': '💂ðŸ¾', ':guard_tone4:': '💂ðŸ¾', ':guardsman_tone5:': '💂ðŸ¿', ':guard_tone5:': '💂ðŸ¿', ':raised_hand_with_fingers_splayed_tone1:': 'ðŸ–ï¸ðŸ»', ':hand_splayed_tone1:': 'ðŸ–ï¸ðŸ»', ':raised_hand_with_fingers_splayed_tone2:': 'ðŸ–ï¸ðŸ¼', ':hand_splayed_tone2:': 'ðŸ–ï¸ðŸ¼', ':raised_hand_with_fingers_splayed_tone3:': 'ðŸ–ï¸ðŸ½', ':hand_splayed_tone3:': 'ðŸ–ï¸ðŸ½', ':raised_hand_with_fingers_splayed_tone4:': 'ðŸ–ï¸ðŸ¾', ':hand_splayed_tone4:': 'ðŸ–ï¸ðŸ¾', ':raised_hand_with_fingers_splayed_tone5:': 'ðŸ–ï¸ðŸ¿', ':hand_splayed_tone5:': 'ðŸ–ï¸ðŸ¿', ':horse_racing_tone1:': 'ðŸ‡ðŸ»', ':horse_racing_tone2:': 'ðŸ‡ðŸ¼', ':horse_racing_tone3:': 'ðŸ‡ðŸ½', ':horse_racing_tone4:': 'ðŸ‡ðŸ¾', ':horse_racing_tone5:': 'ðŸ‡ðŸ¿', ':left_fist_tone1:': '🤛ðŸ»', ':left_facing_fist_tone1:': '🤛ðŸ»', ':left_fist_tone2:': '🤛ðŸ¼', ':left_facing_fist_tone2:': '🤛ðŸ¼', ':left_fist_tone3:': '🤛ðŸ½', ':left_facing_fist_tone3:': '🤛ðŸ½', ':left_fist_tone4:': '🤛ðŸ¾', ':left_facing_fist_tone4:': '🤛ðŸ¾', ':left_fist_tone5:': '🤛ðŸ¿', ':left_facing_fist_tone5:': '🤛ðŸ¿', ':leg_light_skin_tone:': '🦵ðŸ»', ':leg_tone1:': '🦵ðŸ»', ':leg_medium_light_skin_tone:': '🦵ðŸ¼', ':leg_tone2:': '🦵ðŸ¼', ':leg_medium_skin_tone:': '🦵ðŸ½', ':leg_tone3:': '🦵ðŸ½', ':leg_medium_dark_skin_tone:': '🦵ðŸ¾', ':leg_tone4:': '🦵ðŸ¾', ':leg_dark_skin_tone:': '🦵ðŸ¿', ':leg_tone5:': '🦵ðŸ¿', ':man_in_business_suit_levitating_tone1:': '🕴ï¸ðŸ»', ':man_in_business_suit_levitating_light_skin_tone:': '🕴ï¸ðŸ»', ':levitate_tone1:': '🕴ï¸ðŸ»', ':man_in_business_suit_levitating_tone2:': '🕴ï¸ðŸ¼', ':man_in_business_suit_levitating_medium_light_skin_tone:': '🕴ï¸ðŸ¼', ':levitate_tone2:': '🕴ï¸ðŸ¼', ':man_in_business_suit_levitating_tone3:': '🕴ï¸ðŸ½', ':man_in_business_suit_levitating_medium_skin_tone:': '🕴ï¸ðŸ½', ':levitate_tone3:': '🕴ï¸ðŸ½', ':man_in_business_suit_levitating_tone4:': '🕴ï¸ðŸ¾', ':man_in_business_suit_levitating_medium_dark_skin_tone:': '🕴ï¸ðŸ¾', ':levitate_tone4:': '🕴ï¸ðŸ¾', ':man_in_business_suit_levitating_tone5:': '🕴ï¸ðŸ¿', ':man_in_business_suit_levitating_dark_skin_tone:': '🕴ï¸ðŸ¿', ':levitate_tone5:': '🕴ï¸ðŸ¿', ':love_you_gesture_light_skin_tone:': '🤟ðŸ»', ':love_you_gesture_tone1:': '🤟ðŸ»', ':love_you_gesture_medium_light_skin_tone:': '🤟ðŸ¼', ':love_you_gesture_tone2:': '🤟ðŸ¼', ':love_you_gesture_medium_skin_tone:': '🤟ðŸ½', ':love_you_gesture_tone3:': '🤟ðŸ½', ':love_you_gesture_medium_dark_skin_tone:': '🤟ðŸ¾', ':love_you_gesture_tone4:': '🤟ðŸ¾', ':love_you_gesture_dark_skin_tone:': '🤟ðŸ¿', ':love_you_gesture_tone5:': '🤟ðŸ¿', ':mage_light_skin_tone:': '🧙ðŸ»', ':mage_tone1:': '🧙ðŸ»', ':mage_medium_light_skin_tone:': '🧙ðŸ¼', ':mage_tone2:': '🧙ðŸ¼', ':mage_medium_skin_tone:': '🧙ðŸ½', ':mage_tone3:': '🧙ðŸ½', ':mage_medium_dark_skin_tone:': '🧙ðŸ¾', ':mage_tone4:': '🧙ðŸ¾', ':mage_dark_skin_tone:': '🧙ðŸ¿', ':mage_tone5:': '🧙ðŸ¿', ':man_artist:': '👨â€ðŸŽ¨', ':man_astronaut:': '👨â€ðŸš€', ':man_bald:': '👨â€ðŸ¦²', ':man_cook:': '👨â€ðŸ³', ':man_curly_haired:': '👨â€ðŸ¦±', ':male_dancer_tone1:': '🕺ðŸ»', ':man_dancing_tone1:': '🕺ðŸ»', ':male_dancer_tone2:': '🕺ðŸ¼', ':man_dancing_tone2:': '🕺ðŸ¼', ':male_dancer_tone3:': '🕺ðŸ½', ':man_dancing_tone3:': '🕺ðŸ½', ':male_dancer_tone4:': '🕺ðŸ¾', ':man_dancing_tone4:': '🕺ðŸ¾', ':male_dancer_tone5:': '🕺ðŸ¿', ':man_dancing_tone5:': '🕺ðŸ¿', ':man_factory_worker:': '👨â€ðŸ', ':man_farmer:': '👨â€ðŸŒ¾', ':man_firefighter:': '👨â€ðŸš’', ':man_in_manual_wheelchair:': '👨â€ðŸ¦½', ':man_in_motorized_wheelchair:': '👨â€ðŸ¦¼', ':tuxedo_tone1:': '🤵ðŸ»', ':man_in_tuxedo_tone1:': '🤵ðŸ»', ':tuxedo_tone2:': '🤵ðŸ¼', ':man_in_tuxedo_tone2:': '🤵ðŸ¼', ':tuxedo_tone3:': '🤵ðŸ½', ':man_in_tuxedo_tone3:': '🤵ðŸ½', ':tuxedo_tone4:': '🤵ðŸ¾', ':man_in_tuxedo_tone4:': '🤵ðŸ¾', ':tuxedo_tone5:': '🤵ðŸ¿', ':man_in_tuxedo_tone5:': '🤵ðŸ¿', ':man_mechanic:': '👨â€ðŸ”§', ':man_office_worker:': '👨â€ðŸ’¼', ':man_red_haired:': '👨â€ðŸ¦°', ':man_scientist:': '👨â€ðŸ”¬', ':man_singer:': '👨â€ðŸŽ¤', ':man_student:': '👨â€ðŸŽ“', ':man_teacher:': '👨â€ðŸ«', ':man_technologist:': '👨â€ðŸ’»', ':man_tone1:': '👨ðŸ»', ':man_tone2:': '👨ðŸ¼', ':man_tone3:': '👨ðŸ½', ':man_tone4:': '👨ðŸ¾', ':man_tone5:': '👨ðŸ¿', ':man_white_haired:': '👨â€ðŸ¦³', ':man_with_gua_pi_mao_tone1:': '👲ðŸ»', ':man_with_chinese_cap_tone1:': '👲ðŸ»', ':man_with_gua_pi_mao_tone2:': '👲ðŸ¼', ':man_with_chinese_cap_tone2:': '👲ðŸ¼', ':man_with_gua_pi_mao_tone3:': '👲ðŸ½', ':man_with_chinese_cap_tone3:': '👲ðŸ½', ':man_with_gua_pi_mao_tone4:': '👲ðŸ¾', ':man_with_chinese_cap_tone4:': '👲ðŸ¾', ':man_with_gua_pi_mao_tone5:': '👲ðŸ¿', ':man_with_chinese_cap_tone5:': '👲ðŸ¿', ':man_with_probing_cane:': '👨â€ðŸ¦¯', ':men_holding_hands_light_skin_tone:': '👬ðŸ»', ':men_holding_hands_tone1:': '👬ðŸ»', ':men_holding_hands_medium_light_skin_tone:': '👬ðŸ¼', ':men_holding_hands_tone2:': '👬ðŸ¼', ':men_holding_hands_medium_skin_tone:': '👬ðŸ½', ':men_holding_hands_tone3:': '👬ðŸ½', ':men_holding_hands_medium_dark_skin_tone:': '👬ðŸ¾', ':men_holding_hands_tone4:': '👬ðŸ¾', ':men_holding_hands_dark_skin_tone:': '👬ðŸ¿', ':men_holding_hands_tone5:': '👬ðŸ¿', ':merperson_light_skin_tone:': '🧜ðŸ»', ':merperson_tone1:': '🧜ðŸ»', ':merperson_medium_light_skin_tone:': '🧜ðŸ¼', ':merperson_tone2:': '🧜ðŸ¼', ':merperson_medium_skin_tone:': '🧜ðŸ½', ':merperson_tone3:': '🧜ðŸ½', ':merperson_medium_dark_skin_tone:': '🧜ðŸ¾', ':merperson_tone4:': '🧜ðŸ¾', ':merperson_dark_skin_tone:': '🧜ðŸ¿', ':merperson_tone5:': '🧜ðŸ¿', ':sign_of_the_horns_tone1:': '🤘ðŸ»', ':metal_tone1:': '🤘ðŸ»', ':sign_of_the_horns_tone2:': '🤘ðŸ¼', ':metal_tone2:': '🤘ðŸ¼', ':sign_of_the_horns_tone3:': '🤘ðŸ½', ':metal_tone3:': '🤘ðŸ½', ':sign_of_the_horns_tone4:': '🤘ðŸ¾', ':metal_tone4:': '🤘ðŸ¾', ':sign_of_the_horns_tone5:': '🤘ðŸ¿', ':metal_tone5:': '🤘ðŸ¿', ':reversed_hand_with_middle_finger_extended_tone1:': '🖕ðŸ»', ':middle_finger_tone1:': '🖕ðŸ»', ':reversed_hand_with_middle_finger_extended_tone2:': '🖕ðŸ¼', ':middle_finger_tone2:': '🖕ðŸ¼', ':reversed_hand_with_middle_finger_extended_tone3:': '🖕ðŸ½', ':middle_finger_tone3:': '🖕ðŸ½', ':reversed_hand_with_middle_finger_extended_tone4:': '🖕ðŸ¾', ':middle_finger_tone4:': '🖕ðŸ¾', ':reversed_hand_with_middle_finger_extended_tone5:': '🖕ðŸ¿', ':middle_finger_tone5:': '🖕ðŸ¿', ':mother_christmas_tone1:': '🤶ðŸ»', ':mrs_claus_tone1:': '🤶ðŸ»', ':mother_christmas_tone2:': '🤶ðŸ¼', ':mrs_claus_tone2:': '🤶ðŸ¼', ':mother_christmas_tone3:': '🤶ðŸ½', ':mrs_claus_tone3:': '🤶ðŸ½', ':mother_christmas_tone4:': '🤶ðŸ¾', ':mrs_claus_tone4:': '🤶ðŸ¾', ':mother_christmas_tone5:': '🤶ðŸ¿', ':mrs_claus_tone5:': '🤶ðŸ¿', ':muscle_tone1:': '💪ðŸ»', ':muscle_tone2:': '💪ðŸ¼', ':muscle_tone3:': '💪ðŸ½', ':muscle_tone4:': '💪ðŸ¾', ':muscle_tone5:': '💪ðŸ¿', ':nail_care_tone1:': '💅ðŸ»', ':nail_care_tone2:': '💅ðŸ¼', ':nail_care_tone3:': '💅ðŸ½', ':nail_care_tone4:': '💅ðŸ¾', ':nail_care_tone5:': '💅ðŸ¿', ':nose_tone1:': '👃ðŸ»', ':nose_tone2:': '👃ðŸ¼', ':nose_tone3:': '👃ðŸ½', ':nose_tone4:': '👃ðŸ¾', ':nose_tone5:': '👃ðŸ¿', ':ok_hand_tone1:': '👌ðŸ»', ':ok_hand_tone2:': '👌ðŸ¼', ':ok_hand_tone3:': '👌ðŸ½', ':ok_hand_tone4:': '👌ðŸ¾', ':ok_hand_tone5:': '👌ðŸ¿', ':older_adult_light_skin_tone:': '🧓ðŸ»', ':older_adult_tone1:': '🧓ðŸ»', ':older_adult_medium_light_skin_tone:': '🧓ðŸ¼', ':older_adult_tone2:': '🧓ðŸ¼', ':older_adult_medium_skin_tone:': '🧓ðŸ½', ':older_adult_tone3:': '🧓ðŸ½', ':older_adult_medium_dark_skin_tone:': '🧓ðŸ¾', ':older_adult_tone4:': '🧓ðŸ¾', ':older_adult_dark_skin_tone:': '🧓ðŸ¿', ':older_adult_tone5:': '🧓ðŸ¿', ':older_man_tone1:': '👴ðŸ»', ':older_man_tone2:': '👴ðŸ¼', ':older_man_tone3:': '👴ðŸ½', ':older_man_tone4:': '👴ðŸ¾', ':older_man_tone5:': '👴ðŸ¿', ':grandma_tone1:': '👵ðŸ»', ':older_woman_tone1:': '👵ðŸ»', ':grandma_tone2:': '👵ðŸ¼', ':older_woman_tone2:': '👵ðŸ¼', ':grandma_tone3:': '👵ðŸ½', ':older_woman_tone3:': '👵ðŸ½', ':grandma_tone4:': '👵ðŸ¾', ':older_woman_tone4:': '👵ðŸ¾', ':grandma_tone5:': '👵ðŸ¿', ':older_woman_tone5:': '👵ðŸ¿', ':open_hands_tone1:': 'ðŸ‘ðŸ»', ':open_hands_tone2:': 'ðŸ‘ðŸ¼', ':open_hands_tone3:': 'ðŸ‘ðŸ½', ':open_hands_tone4:': 'ðŸ‘ðŸ¾', ':open_hands_tone5:': 'ðŸ‘ðŸ¿', ':palms_up_together_light_skin_tone:': '🤲ðŸ»', ':palms_up_together_tone1:': '🤲ðŸ»', ':palms_up_together_medium_light_skin_tone:': '🤲ðŸ¼', ':palms_up_together_tone2:': '🤲ðŸ¼', ':palms_up_together_medium_skin_tone:': '🤲ðŸ½', ':palms_up_together_tone3:': '🤲ðŸ½', ':palms_up_together_medium_dark_skin_tone:': '🤲ðŸ¾', ':palms_up_together_tone4:': '🤲ðŸ¾', ':palms_up_together_dark_skin_tone:': '🤲ðŸ¿', ':palms_up_together_tone5:': '🤲ðŸ¿', ':bicyclist_tone1:': '🚴ðŸ»', ':person_biking_tone1:': '🚴ðŸ»', ':bicyclist_tone2:': '🚴ðŸ¼', ':person_biking_tone2:': '🚴ðŸ¼', ':bicyclist_tone3:': '🚴ðŸ½', ':person_biking_tone3:': '🚴ðŸ½', ':bicyclist_tone4:': '🚴ðŸ¾', ':person_biking_tone4:': '🚴ðŸ¾', ':bicyclist_tone5:': '🚴ðŸ¿', ':person_biking_tone5:': '🚴ðŸ¿', ':bow_tone1:': '🙇ðŸ»', ':person_bowing_tone1:': '🙇ðŸ»', ':bow_tone2:': '🙇ðŸ¼', ':person_bowing_tone2:': '🙇ðŸ¼', ':bow_tone3:': '🙇ðŸ½', ':person_bowing_tone3:': '🙇ðŸ½', ':bow_tone4:': '🙇ðŸ¾', ':person_bowing_tone4:': '🙇ðŸ¾', ':bow_tone5:': '🙇ðŸ¿', ':person_bowing_tone5:': '🙇ðŸ¿', ':person_climbing_light_skin_tone:': '🧗ðŸ»', ':person_climbing_tone1:': '🧗ðŸ»', ':person_climbing_medium_light_skin_tone:': '🧗ðŸ¼', ':person_climbing_tone2:': '🧗ðŸ¼', ':person_climbing_medium_skin_tone:': '🧗ðŸ½', ':person_climbing_tone3:': '🧗ðŸ½', ':person_climbing_medium_dark_skin_tone:': '🧗ðŸ¾', ':person_climbing_tone4:': '🧗ðŸ¾', ':person_climbing_dark_skin_tone:': '🧗ðŸ¿', ':person_climbing_tone5:': '🧗ðŸ¿', ':cartwheel_tone1:': '🤸ðŸ»', ':person_doing_cartwheel_tone1:': '🤸ðŸ»', ':cartwheel_tone2:': '🤸ðŸ¼', ':person_doing_cartwheel_tone2:': '🤸ðŸ¼', ':cartwheel_tone3:': '🤸ðŸ½', ':person_doing_cartwheel_tone3:': '🤸ðŸ½', ':cartwheel_tone4:': '🤸ðŸ¾', ':person_doing_cartwheel_tone4:': '🤸ðŸ¾', ':cartwheel_tone5:': '🤸ðŸ¿', ':person_doing_cartwheel_tone5:': '🤸ðŸ¿', ':face_palm_tone1:': '🤦ðŸ»', ':facepalm_tone1:': '🤦ðŸ»', ':person_facepalming_tone1:': '🤦ðŸ»', ':face_palm_tone2:': '🤦ðŸ¼', ':facepalm_tone2:': '🤦ðŸ¼', ':person_facepalming_tone2:': '🤦ðŸ¼', ':face_palm_tone3:': '🤦ðŸ½', ':facepalm_tone3:': '🤦ðŸ½', ':person_facepalming_tone3:': '🤦ðŸ½', ':face_palm_tone4:': '🤦ðŸ¾', ':facepalm_tone4:': '🤦ðŸ¾', ':person_facepalming_tone4:': '🤦ðŸ¾', ':face_palm_tone5:': '🤦ðŸ¿', ':facepalm_tone5:': '🤦ðŸ¿', ':person_facepalming_tone5:': '🤦ðŸ¿', ':person_frowning_tone1:': 'ðŸ™ðŸ»', ':person_frowning_tone2:': 'ðŸ™ðŸ¼', ':person_frowning_tone3:': 'ðŸ™ðŸ½', ':person_frowning_tone4:': 'ðŸ™ðŸ¾', ':person_frowning_tone5:': 'ðŸ™ðŸ¿', ':no_good_tone1:': '🙅ðŸ»', ':person_gesturing_no_tone1:': '🙅ðŸ»', ':no_good_tone2:': '🙅ðŸ¼', ':person_gesturing_no_tone2:': '🙅ðŸ¼', ':no_good_tone3:': '🙅ðŸ½', ':person_gesturing_no_tone3:': '🙅ðŸ½', ':no_good_tone4:': '🙅ðŸ¾', ':person_gesturing_no_tone4:': '🙅ðŸ¾', ':no_good_tone5:': '🙅ðŸ¿', ':person_gesturing_no_tone5:': '🙅ðŸ¿', ':ok_woman_tone1:': '🙆ðŸ»', ':person_gesturing_ok_tone1:': '🙆ðŸ»', ':ok_woman_tone2:': '🙆ðŸ¼', ':person_gesturing_ok_tone2:': '🙆ðŸ¼', ':ok_woman_tone3:': '🙆ðŸ½', ':person_gesturing_ok_tone3:': '🙆ðŸ½', ':ok_woman_tone4:': '🙆ðŸ¾', ':person_gesturing_ok_tone4:': '🙆ðŸ¾', ':ok_woman_tone5:': '🙆ðŸ¿', ':person_gesturing_ok_tone5:': '🙆ðŸ¿', ':haircut_tone1:': '💇ðŸ»', ':person_getting_haircut_tone1:': '💇ðŸ»', ':haircut_tone2:': '💇ðŸ¼', ':person_getting_haircut_tone2:': '💇ðŸ¼', ':haircut_tone3:': '💇ðŸ½', ':person_getting_haircut_tone3:': '💇ðŸ½', ':haircut_tone4:': '💇ðŸ¾', ':person_getting_haircut_tone4:': '💇ðŸ¾', ':haircut_tone5:': '💇ðŸ¿', ':person_getting_haircut_tone5:': '💇ðŸ¿', ':massage_tone1:': '💆ðŸ»', ':person_getting_massage_tone1:': '💆ðŸ»', ':massage_tone2:': '💆ðŸ¼', ':person_getting_massage_tone2:': '💆ðŸ¼', ':massage_tone3:': '💆ðŸ½', ':person_getting_massage_tone3:': '💆ðŸ½', ':massage_tone4:': '💆ðŸ¾', ':person_getting_massage_tone4:': '💆ðŸ¾', ':massage_tone5:': '💆ðŸ¿', ':person_getting_massage_tone5:': '💆ðŸ¿', ':person_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»', ':person_golfing_tone1:': 'ðŸŒï¸ðŸ»', ':person_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼', ':person_golfing_tone2:': 'ðŸŒï¸ðŸ¼', ':person_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½', ':person_golfing_tone3:': 'ðŸŒï¸ðŸ½', ':person_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾', ':person_golfing_tone4:': 'ðŸŒï¸ðŸ¾', ':person_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿', ':person_golfing_tone5:': 'ðŸŒï¸ðŸ¿', ':person_in_bed_light_skin_tone:': '🛌ðŸ»', ':person_in_bed_tone1:': '🛌ðŸ»', ':person_in_bed_medium_light_skin_tone:': '🛌ðŸ¼', ':person_in_bed_tone2:': '🛌ðŸ¼', ':person_in_bed_medium_skin_tone:': '🛌ðŸ½', ':person_in_bed_tone3:': '🛌ðŸ½', ':person_in_bed_medium_dark_skin_tone:': '🛌ðŸ¾', ':person_in_bed_tone4:': '🛌ðŸ¾', ':person_in_bed_dark_skin_tone:': '🛌ðŸ¿', ':person_in_bed_tone5:': '🛌ðŸ¿', ':person_in_lotus_position_light_skin_tone:': '🧘ðŸ»', ':person_in_lotus_position_tone1:': '🧘ðŸ»', ':person_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼', ':person_in_lotus_position_tone2:': '🧘ðŸ¼', ':person_in_lotus_position_medium_skin_tone:': '🧘ðŸ½', ':person_in_lotus_position_tone3:': '🧘ðŸ½', ':person_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾', ':person_in_lotus_position_tone4:': '🧘ðŸ¾', ':person_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿', ':person_in_lotus_position_tone5:': '🧘ðŸ¿', ':person_in_steamy_room_light_skin_tone:': '🧖ðŸ»', ':person_in_steamy_room_tone1:': '🧖ðŸ»', ':person_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼', ':person_in_steamy_room_tone2:': '🧖ðŸ¼', ':person_in_steamy_room_medium_skin_tone:': '🧖ðŸ½', ':person_in_steamy_room_tone3:': '🧖ðŸ½', ':person_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾', ':person_in_steamy_room_tone4:': '🧖ðŸ¾', ':person_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿', ':person_in_steamy_room_tone5:': '🧖ðŸ¿', ':juggling_tone1:': '🤹ðŸ»', ':juggler_tone1:': '🤹ðŸ»', ':person_juggling_tone1:': '🤹ðŸ»', ':juggling_tone2:': '🤹ðŸ¼', ':juggler_tone2:': '🤹ðŸ¼', ':person_juggling_tone2:': '🤹ðŸ¼', ':juggling_tone3:': '🤹ðŸ½', ':juggler_tone3:': '🤹ðŸ½', ':person_juggling_tone3:': '🤹ðŸ½', ':juggling_tone4:': '🤹ðŸ¾', ':juggler_tone4:': '🤹ðŸ¾', ':person_juggling_tone4:': '🤹ðŸ¾', ':juggling_tone5:': '🤹ðŸ¿', ':juggler_tone5:': '🤹ðŸ¿', ':person_juggling_tone5:': '🤹ðŸ¿', ':person_kneeling_light_skin_tone:': '🧎ðŸ»', ':person_kneeling_tone1:': '🧎ðŸ»', ':person_kneeling_medium_light_skin_tone:': '🧎ðŸ¼', ':person_kneeling_tone2:': '🧎ðŸ¼', ':person_kneeling_medium_skin_tone:': '🧎ðŸ½', ':person_kneeling_tone3:': '🧎ðŸ½', ':person_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾', ':person_kneeling_tone4:': '🧎ðŸ¾', ':person_kneeling_dark_skin_tone:': '🧎ðŸ¿', ':person_kneeling_tone5:': '🧎ðŸ¿', ':lifter_tone1:': 'ðŸ‹ï¸ðŸ»', ':weight_lifter_tone1:': 'ðŸ‹ï¸ðŸ»', ':person_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»', ':lifter_tone2:': 'ðŸ‹ï¸ðŸ¼', ':weight_lifter_tone2:': 'ðŸ‹ï¸ðŸ¼', ':person_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼', ':lifter_tone3:': 'ðŸ‹ï¸ðŸ½', ':weight_lifter_tone3:': 'ðŸ‹ï¸ðŸ½', ':person_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½', ':lifter_tone4:': 'ðŸ‹ï¸ðŸ¾', ':weight_lifter_tone4:': 'ðŸ‹ï¸ðŸ¾', ':person_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾', ':lifter_tone5:': 'ðŸ‹ï¸ðŸ¿', ':weight_lifter_tone5:': 'ðŸ‹ï¸ðŸ¿', ':person_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿', ':mountain_bicyclist_tone1:': '🚵ðŸ»', ':person_mountain_biking_tone1:': '🚵ðŸ»', ':mountain_bicyclist_tone2:': '🚵ðŸ¼', ':person_mountain_biking_tone2:': '🚵ðŸ¼', ':mountain_bicyclist_tone3:': '🚵ðŸ½', ':person_mountain_biking_tone3:': '🚵ðŸ½', ':mountain_bicyclist_tone4:': '🚵ðŸ¾', ':person_mountain_biking_tone4:': '🚵ðŸ¾', ':mountain_bicyclist_tone5:': '🚵ðŸ¿', ':person_mountain_biking_tone5:': '🚵ðŸ¿', ':handball_tone1:': '🤾ðŸ»', ':person_playing_handball_tone1:': '🤾ðŸ»', ':handball_tone2:': '🤾ðŸ¼', ':person_playing_handball_tone2:': '🤾ðŸ¼', ':handball_tone3:': '🤾ðŸ½', ':person_playing_handball_tone3:': '🤾ðŸ½', ':handball_tone4:': '🤾ðŸ¾', ':person_playing_handball_tone4:': '🤾ðŸ¾', ':handball_tone5:': '🤾ðŸ¿', ':person_playing_handball_tone5:': '🤾ðŸ¿', ':water_polo_tone1:': '🤽ðŸ»', ':person_playing_water_polo_tone1:': '🤽ðŸ»', ':water_polo_tone2:': '🤽ðŸ¼', ':person_playing_water_polo_tone2:': '🤽ðŸ¼', ':water_polo_tone3:': '🤽ðŸ½', ':person_playing_water_polo_tone3:': '🤽ðŸ½', ':water_polo_tone4:': '🤽ðŸ¾', ':person_playing_water_polo_tone4:': '🤽ðŸ¾', ':water_polo_tone5:': '🤽ðŸ¿', ':person_playing_water_polo_tone5:': '🤽ðŸ¿', ':person_with_pouting_face_tone1:': '🙎ðŸ»', ':person_pouting_tone1:': '🙎ðŸ»', ':person_with_pouting_face_tone2:': '🙎ðŸ¼', ':person_pouting_tone2:': '🙎ðŸ¼', ':person_with_pouting_face_tone3:': '🙎ðŸ½', ':person_pouting_tone3:': '🙎ðŸ½', ':person_with_pouting_face_tone4:': '🙎ðŸ¾', ':person_pouting_tone4:': '🙎ðŸ¾', ':person_with_pouting_face_tone5:': '🙎ðŸ¿', ':person_pouting_tone5:': '🙎ðŸ¿', ':raising_hand_tone1:': '🙋ðŸ»', ':person_raising_hand_tone1:': '🙋ðŸ»', ':raising_hand_tone2:': '🙋ðŸ¼', ':person_raising_hand_tone2:': '🙋ðŸ¼', ':raising_hand_tone3:': '🙋ðŸ½', ':person_raising_hand_tone3:': '🙋ðŸ½', ':raising_hand_tone4:': '🙋ðŸ¾', ':person_raising_hand_tone4:': '🙋ðŸ¾', ':raising_hand_tone5:': '🙋ðŸ¿', ':person_raising_hand_tone5:': '🙋ðŸ¿', ':rowboat_tone1:': '🚣ðŸ»', ':person_rowing_boat_tone1:': '🚣ðŸ»', ':rowboat_tone2:': '🚣ðŸ¼', ':person_rowing_boat_tone2:': '🚣ðŸ¼', ':rowboat_tone3:': '🚣ðŸ½', ':person_rowing_boat_tone3:': '🚣ðŸ½', ':rowboat_tone4:': '🚣ðŸ¾', ':person_rowing_boat_tone4:': '🚣ðŸ¾', ':rowboat_tone5:': '🚣ðŸ¿', ':person_rowing_boat_tone5:': '🚣ðŸ¿', ':runner_tone1:': 'ðŸƒðŸ»', ':person_running_tone1:': 'ðŸƒðŸ»', ':runner_tone2:': 'ðŸƒðŸ¼', ':person_running_tone2:': 'ðŸƒðŸ¼', ':runner_tone3:': 'ðŸƒðŸ½', ':person_running_tone3:': 'ðŸƒðŸ½', ':runner_tone4:': 'ðŸƒðŸ¾', ':person_running_tone4:': 'ðŸƒðŸ¾', ':runner_tone5:': 'ðŸƒðŸ¿', ':person_running_tone5:': 'ðŸƒðŸ¿', ':shrug_tone1:': '🤷ðŸ»', ':person_shrugging_tone1:': '🤷ðŸ»', ':shrug_tone2:': '🤷ðŸ¼', ':person_shrugging_tone2:': '🤷ðŸ¼', ':shrug_tone3:': '🤷ðŸ½', ':person_shrugging_tone3:': '🤷ðŸ½', ':shrug_tone4:': '🤷ðŸ¾', ':person_shrugging_tone4:': '🤷ðŸ¾', ':shrug_tone5:': '🤷ðŸ¿', ':person_shrugging_tone5:': '🤷ðŸ¿', ':person_standing_light_skin_tone:': 'ðŸ§ðŸ»', ':person_standing_tone1:': 'ðŸ§ðŸ»', ':person_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼', ':person_standing_tone2:': 'ðŸ§ðŸ¼', ':person_standing_medium_skin_tone:': 'ðŸ§ðŸ½', ':person_standing_tone3:': 'ðŸ§ðŸ½', ':person_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', ':person_standing_tone4:': 'ðŸ§ðŸ¾', ':person_standing_dark_skin_tone:': 'ðŸ§ðŸ¿', ':person_standing_tone5:': 'ðŸ§ðŸ¿', ':surfer_tone1:': 'ðŸ„ðŸ»', ':person_surfing_tone1:': 'ðŸ„ðŸ»', ':surfer_tone2:': 'ðŸ„ðŸ¼', ':person_surfing_tone2:': 'ðŸ„ðŸ¼', ':surfer_tone3:': 'ðŸ„ðŸ½', ':person_surfing_tone3:': 'ðŸ„ðŸ½', ':surfer_tone4:': 'ðŸ„ðŸ¾', ':person_surfing_tone4:': 'ðŸ„ðŸ¾', ':surfer_tone5:': 'ðŸ„ðŸ¿', ':person_surfing_tone5:': 'ðŸ„ðŸ¿', ':swimmer_tone1:': 'ðŸŠðŸ»', ':person_swimming_tone1:': 'ðŸŠðŸ»', ':swimmer_tone2:': 'ðŸŠðŸ¼', ':person_swimming_tone2:': 'ðŸŠðŸ¼', ':swimmer_tone3:': 'ðŸŠðŸ½', ':person_swimming_tone3:': 'ðŸŠðŸ½', ':swimmer_tone4:': 'ðŸŠðŸ¾', ':person_swimming_tone4:': 'ðŸŠðŸ¾', ':swimmer_tone5:': 'ðŸŠðŸ¿', ':person_swimming_tone5:': 'ðŸŠðŸ¿', ':information_desk_person_tone1:': 'ðŸ’ðŸ»', ':person_tipping_hand_tone1:': 'ðŸ’ðŸ»', ':information_desk_person_tone2:': 'ðŸ’ðŸ¼', ':person_tipping_hand_tone2:': 'ðŸ’ðŸ¼', ':information_desk_person_tone3:': 'ðŸ’ðŸ½', ':person_tipping_hand_tone3:': 'ðŸ’ðŸ½', ':information_desk_person_tone4:': 'ðŸ’ðŸ¾', ':person_tipping_hand_tone4:': 'ðŸ’ðŸ¾', ':information_desk_person_tone5:': 'ðŸ’ðŸ¿', ':person_tipping_hand_tone5:': 'ðŸ’ðŸ¿', ':walking_tone1:': '🚶ðŸ»', ':person_walking_tone1:': '🚶ðŸ»', ':walking_tone2:': '🚶ðŸ¼', ':person_walking_tone2:': '🚶ðŸ¼', ':walking_tone3:': '🚶ðŸ½', ':person_walking_tone3:': '🚶ðŸ½', ':walking_tone4:': '🚶ðŸ¾', ':person_walking_tone4:': '🚶ðŸ¾', ':walking_tone5:': '🚶ðŸ¿', ':person_walking_tone5:': '🚶ðŸ¿', ':man_with_turban_tone1:': '👳ðŸ»', ':person_wearing_turban_tone1:': '👳ðŸ»', ':man_with_turban_tone2:': '👳ðŸ¼', ':person_wearing_turban_tone2:': '👳ðŸ¼', ':man_with_turban_tone3:': '👳ðŸ½', ':person_wearing_turban_tone3:': '👳ðŸ½', ':man_with_turban_tone4:': '👳ðŸ¾', ':person_wearing_turban_tone4:': '👳ðŸ¾', ':man_with_turban_tone5:': '👳ðŸ¿', ':person_wearing_turban_tone5:': '👳ðŸ¿', ':pinching_hand_light_skin_tone:': 'ðŸ¤ðŸ»', ':pinching_hand_tone1:': 'ðŸ¤ðŸ»', ':pinching_hand_medium_light_skin_tone:': 'ðŸ¤ðŸ¼', ':pinching_hand_tone2:': 'ðŸ¤ðŸ¼', ':pinching_hand_medium_skin_tone:': 'ðŸ¤ðŸ½', ':pinching_hand_tone3:': 'ðŸ¤ðŸ½', ':pinching_hand_medium_dark_skin_tone:': 'ðŸ¤ðŸ¾', ':pinching_hand_tone4:': 'ðŸ¤ðŸ¾', ':pinching_hand_dark_skin_tone:': 'ðŸ¤ðŸ¿', ':pinching_hand_tone5:': 'ðŸ¤ðŸ¿', ':point_down_tone1:': '👇ðŸ»', ':point_down_tone2:': '👇ðŸ¼', ':point_down_tone3:': '👇ðŸ½', ':point_down_tone4:': '👇ðŸ¾', ':point_down_tone5:': '👇ðŸ¿', ':point_left_tone1:': '👈ðŸ»', ':point_left_tone2:': '👈ðŸ¼', ':point_left_tone3:': '👈ðŸ½', ':point_left_tone4:': '👈ðŸ¾', ':point_left_tone5:': '👈ðŸ¿', ':point_right_tone1:': '👉ðŸ»', ':point_right_tone2:': '👉ðŸ¼', ':point_right_tone3:': '👉ðŸ½', ':point_right_tone4:': '👉ðŸ¾', ':point_right_tone5:': '👉ðŸ¿', ':point_up_2_tone1:': '👆ðŸ»', ':point_up_2_tone2:': '👆ðŸ¼', ':point_up_2_tone3:': '👆ðŸ½', ':point_up_2_tone4:': '👆ðŸ¾', ':point_up_2_tone5:': '👆ðŸ¿', ':cop_tone1:': '👮ðŸ»', ':police_officer_tone1:': '👮ðŸ»', ':cop_tone2:': '👮ðŸ¼', ':police_officer_tone2:': '👮ðŸ¼', ':cop_tone3:': '👮ðŸ½', ':police_officer_tone3:': '👮ðŸ½', ':cop_tone4:': '👮ðŸ¾', ':police_officer_tone4:': '👮ðŸ¾', ':cop_tone5:': '👮ðŸ¿', ':police_officer_tone5:': '👮ðŸ¿', ':pray_tone1:': 'ðŸ™ðŸ»', ':pray_tone2:': 'ðŸ™ðŸ¼', ':pray_tone3:': 'ðŸ™ðŸ½', ':pray_tone4:': 'ðŸ™ðŸ¾', ':pray_tone5:': 'ðŸ™ðŸ¿', ':expecting_woman_tone1:': '🤰ðŸ»', ':pregnant_woman_tone1:': '🤰ðŸ»', ':expecting_woman_tone2:': '🤰ðŸ¼', ':pregnant_woman_tone2:': '🤰ðŸ¼', ':expecting_woman_tone3:': '🤰ðŸ½', ':pregnant_woman_tone3:': '🤰ðŸ½', ':expecting_woman_tone4:': '🤰ðŸ¾', ':pregnant_woman_tone4:': '🤰ðŸ¾', ':expecting_woman_tone5:': '🤰ðŸ¿', ':pregnant_woman_tone5:': '🤰ðŸ¿', ':prince_tone1:': '🤴ðŸ»', ':prince_tone2:': '🤴ðŸ¼', ':prince_tone3:': '🤴ðŸ½', ':prince_tone4:': '🤴ðŸ¾', ':prince_tone5:': '🤴ðŸ¿', ':princess_tone1:': '👸ðŸ»', ':princess_tone2:': '👸ðŸ¼', ':princess_tone3:': '👸ðŸ½', ':princess_tone4:': '👸ðŸ¾', ':princess_tone5:': '👸ðŸ¿', ':punch_tone1:': '👊ðŸ»', ':punch_tone2:': '👊ðŸ¼', ':punch_tone3:': '👊ðŸ½', ':punch_tone4:': '👊ðŸ¾', ':punch_tone5:': '👊ðŸ¿', ':gay_pride_flag:': 'ðŸ³ï¸â€ðŸŒˆ', ':rainbow_flag:': 'ðŸ³ï¸â€ðŸŒˆ', ':back_of_hand_tone1:': '🤚ðŸ»', ':raised_back_of_hand_tone1:': '🤚ðŸ»', ':back_of_hand_tone2:': '🤚ðŸ¼', ':raised_back_of_hand_tone2:': '🤚ðŸ¼', ':back_of_hand_tone3:': '🤚ðŸ½', ':raised_back_of_hand_tone3:': '🤚ðŸ½', ':back_of_hand_tone4:': '🤚ðŸ¾', ':raised_back_of_hand_tone4:': '🤚ðŸ¾', ':back_of_hand_tone5:': '🤚ðŸ¿', ':raised_back_of_hand_tone5:': '🤚ðŸ¿', ':raised_hands_tone1:': '🙌ðŸ»', ':raised_hands_tone2:': '🙌ðŸ¼', ':raised_hands_tone3:': '🙌ðŸ½', ':raised_hands_tone4:': '🙌ðŸ¾', ':raised_hands_tone5:': '🙌ðŸ¿', ':right_fist_tone1:': '🤜ðŸ»', ':right_facing_fist_tone1:': '🤜ðŸ»', ':right_fist_tone2:': '🤜ðŸ¼', ':right_facing_fist_tone2:': '🤜ðŸ¼', ':right_fist_tone3:': '🤜ðŸ½', ':right_facing_fist_tone3:': '🤜ðŸ½', ':right_fist_tone4:': '🤜ðŸ¾', ':right_facing_fist_tone4:': '🤜ðŸ¾', ':right_fist_tone5:': '🤜ðŸ¿', ':right_facing_fist_tone5:': '🤜ðŸ¿', ':santa_tone1:': '🎅ðŸ»', ':santa_tone2:': '🎅ðŸ¼', ':santa_tone3:': '🎅ðŸ½', ':santa_tone4:': '🎅ðŸ¾', ':santa_tone5:': '🎅ðŸ¿', ':selfie_tone1:': '🤳ðŸ»', ':selfie_tone2:': '🤳ðŸ¼', ':selfie_tone3:': '🤳ðŸ½', ':selfie_tone4:': '🤳ðŸ¾', ':selfie_tone5:': '🤳ðŸ¿', ':service_dog:': 'ðŸ•â€ðŸ¦º', ':snowboarder_light_skin_tone:': 'ðŸ‚ðŸ»', ':snowboarder_tone1:': 'ðŸ‚ðŸ»', ':snowboarder_medium_light_skin_tone:': 'ðŸ‚ðŸ¼', ':snowboarder_tone2:': 'ðŸ‚ðŸ¼', ':snowboarder_medium_skin_tone:': 'ðŸ‚ðŸ½', ':snowboarder_tone3:': 'ðŸ‚ðŸ½', ':snowboarder_medium_dark_skin_tone:': 'ðŸ‚ðŸ¾', ':snowboarder_tone4:': 'ðŸ‚ðŸ¾', ':snowboarder_dark_skin_tone:': 'ðŸ‚ðŸ¿', ':snowboarder_tone5:': 'ðŸ‚ðŸ¿', ':superhero_light_skin_tone:': '🦸ðŸ»', ':superhero_tone1:': '🦸ðŸ»', ':superhero_medium_light_skin_tone:': '🦸ðŸ¼', ':superhero_tone2:': '🦸ðŸ¼', ':superhero_medium_skin_tone:': '🦸ðŸ½', ':superhero_tone3:': '🦸ðŸ½', ':superhero_medium_dark_skin_tone:': '🦸ðŸ¾', ':superhero_tone4:': '🦸ðŸ¾', ':superhero_dark_skin_tone:': '🦸ðŸ¿', ':superhero_tone5:': '🦸ðŸ¿', ':supervillain_light_skin_tone:': '🦹ðŸ»', ':supervillain_tone1:': '🦹ðŸ»', ':supervillain_medium_light_skin_tone:': '🦹ðŸ¼', ':supervillain_tone2:': '🦹ðŸ¼', ':supervillain_medium_skin_tone:': '🦹ðŸ½', ':supervillain_tone3:': '🦹ðŸ½', ':supervillain_medium_dark_skin_tone:': '🦹ðŸ¾', ':supervillain_tone4:': '🦹ðŸ¾', ':supervillain_dark_skin_tone:': '🦹ðŸ¿', ':supervillain_tone5:': '🦹ðŸ¿', ':-1_tone1:': '👎ðŸ»', ':thumbdown_tone1:': '👎ðŸ»', ':thumbsdown_tone1:': '👎ðŸ»', ':-1_tone2:': '👎ðŸ¼', ':thumbdown_tone2:': '👎ðŸ¼', ':thumbsdown_tone2:': '👎ðŸ¼', ':-1_tone3:': '👎ðŸ½', ':thumbdown_tone3:': '👎ðŸ½', ':thumbsdown_tone3:': '👎ðŸ½', ':-1_tone4:': '👎ðŸ¾', ':thumbdown_tone4:': '👎ðŸ¾', ':thumbsdown_tone4:': '👎ðŸ¾', ':-1_tone5:': '👎ðŸ¿', ':thumbdown_tone5:': '👎ðŸ¿', ':thumbsdown_tone5:': '👎ðŸ¿', ':+1_tone1:': 'ðŸ‘ðŸ»', ':thumbup_tone1:': 'ðŸ‘ðŸ»', ':thumbsup_tone1:': 'ðŸ‘ðŸ»', ':+1_tone2:': 'ðŸ‘ðŸ¼', ':thumbup_tone2:': 'ðŸ‘ðŸ¼', ':thumbsup_tone2:': 'ðŸ‘ðŸ¼', ':+1_tone3:': 'ðŸ‘ðŸ½', ':thumbup_tone3:': 'ðŸ‘ðŸ½', ':thumbsup_tone3:': 'ðŸ‘ðŸ½', ':+1_tone4:': 'ðŸ‘ðŸ¾', ':thumbup_tone4:': 'ðŸ‘ðŸ¾', ':thumbsup_tone4:': 'ðŸ‘ðŸ¾', ':+1_tone5:': 'ðŸ‘ðŸ¿', ':thumbup_tone5:': 'ðŸ‘ðŸ¿', ':thumbsup_tone5:': 'ðŸ‘ðŸ¿', ':united_nations:': '🇺🇳', ':vampire_light_skin_tone:': '🧛ðŸ»', ':vampire_tone1:': '🧛ðŸ»', ':vampire_medium_light_skin_tone:': '🧛ðŸ¼', ':vampire_tone2:': '🧛ðŸ¼', ':vampire_medium_skin_tone:': '🧛ðŸ½', ':vampire_tone3:': '🧛ðŸ½', ':vampire_medium_dark_skin_tone:': '🧛ðŸ¾', ':vampire_tone4:': '🧛ðŸ¾', ':vampire_dark_skin_tone:': '🧛ðŸ¿', ':vampire_tone5:': '🧛ðŸ¿', ':raised_hand_with_part_between_middle_and_ring_fingers_tone1:': '🖖ðŸ»', ':vulcan_tone1:': '🖖ðŸ»', ':raised_hand_with_part_between_middle_and_ring_fingers_tone2:': '🖖ðŸ¼', ':vulcan_tone2:': '🖖ðŸ¼', ':raised_hand_with_part_between_middle_and_ring_fingers_tone3:': '🖖ðŸ½', ':vulcan_tone3:': '🖖ðŸ½', ':raised_hand_with_part_between_middle_and_ring_fingers_tone4:': '🖖ðŸ¾', ':vulcan_tone4:': '🖖ðŸ¾', ':raised_hand_with_part_between_middle_and_ring_fingers_tone5:': '🖖ðŸ¿', ':vulcan_tone5:': '🖖ðŸ¿', ':wave_tone1:': '👋ðŸ»', ':wave_tone2:': '👋ðŸ¼', ':wave_tone3:': '👋ðŸ½', ':wave_tone4:': '👋ðŸ¾', ':wave_tone5:': '👋ðŸ¿', ':woman_and_man_holding_hands_light_skin_tone:': '👫ðŸ»', ':woman_and_man_holding_hands_tone1:': '👫ðŸ»', ':woman_and_man_holding_hands_medium_light_skin_tone:': '👫ðŸ¼', ':woman_and_man_holding_hands_tone2:': '👫ðŸ¼', ':woman_and_man_holding_hands_medium_skin_tone:': '👫ðŸ½', ':woman_and_man_holding_hands_tone3:': '👫ðŸ½', ':woman_and_man_holding_hands_medium_dark_skin_tone:': '👫ðŸ¾', ':woman_and_man_holding_hands_tone4:': '👫ðŸ¾', ':woman_and_man_holding_hands_dark_skin_tone:': '👫ðŸ¿', ':woman_and_man_holding_hands_tone5:': '👫ðŸ¿', ':woman_artist:': '👩â€ðŸŽ¨', ':woman_astronaut:': '👩â€ðŸš€', ':woman_bald:': '👩â€ðŸ¦²', ':woman_cook:': '👩â€ðŸ³', ':woman_curly_haired:': '👩â€ðŸ¦±', ':woman_factory_worker:': '👩â€ðŸ', ':woman_farmer:': '👩â€ðŸŒ¾', ':woman_firefighter:': '👩â€ðŸš’', ':woman_in_manual_wheelchair:': '👩â€ðŸ¦½', ':woman_in_motorized_wheelchair:': '👩â€ðŸ¦¼', ':woman_mechanic:': '👩â€ðŸ”§', ':woman_office_worker:': '👩â€ðŸ’¼', ':woman_red_haired:': '👩â€ðŸ¦°', ':woman_scientist:': '👩â€ðŸ”¬', ':woman_singer:': '👩â€ðŸŽ¤', ':woman_student:': '👩â€ðŸŽ“', ':woman_teacher:': '👩â€ðŸ«', ':woman_technologist:': '👩â€ðŸ’»', ':woman_tone1:': '👩ðŸ»', ':woman_tone2:': '👩ðŸ¼', ':woman_tone3:': '👩ðŸ½', ':woman_tone4:': '👩ðŸ¾', ':woman_tone5:': '👩ðŸ¿', ':woman_white_haired:': '👩â€ðŸ¦³', ':woman_with_headscarf_light_skin_tone:': '🧕ðŸ»', ':woman_with_headscarf_tone1:': '🧕ðŸ»', ':woman_with_headscarf_medium_light_skin_tone:': '🧕ðŸ¼', ':woman_with_headscarf_tone2:': '🧕ðŸ¼', ':woman_with_headscarf_medium_skin_tone:': '🧕ðŸ½', ':woman_with_headscarf_tone3:': '🧕ðŸ½', ':woman_with_headscarf_medium_dark_skin_tone:': '🧕ðŸ¾', ':woman_with_headscarf_tone4:': '🧕ðŸ¾', ':woman_with_headscarf_dark_skin_tone:': '🧕ðŸ¿', ':woman_with_headscarf_tone5:': '🧕ðŸ¿', ':woman_with_probing_cane:': '👩â€ðŸ¦¯', ':women_holding_hands_light_skin_tone:': 'ðŸ‘ðŸ»', ':women_holding_hands_tone1:': 'ðŸ‘ðŸ»', ':women_holding_hands_medium_light_skin_tone:': 'ðŸ‘ðŸ¼', ':women_holding_hands_tone2:': 'ðŸ‘ðŸ¼', ':women_holding_hands_medium_skin_tone:': 'ðŸ‘ðŸ½', ':women_holding_hands_tone3:': 'ðŸ‘ðŸ½', ':women_holding_hands_medium_dark_skin_tone:': 'ðŸ‘ðŸ¾', ':women_holding_hands_tone4:': 'ðŸ‘ðŸ¾', ':women_holding_hands_dark_skin_tone:': 'ðŸ‘ðŸ¿', ':women_holding_hands_tone5:': 'ðŸ‘ðŸ¿', ':blond-haired_man:': '👱â€â™‚ï¸', ':blond-haired_woman:': '👱â€â™€ï¸', ':deaf_man:': 'ðŸ§â€â™‚ï¸', ':deaf_woman:': 'ðŸ§â€â™€ï¸', ':fist_tone1:': '✊ðŸ»', ':fist_tone2:': '✊ðŸ¼', ':fist_tone3:': '✊ðŸ½', ':fist_tone4:': '✊ðŸ¾', ':fist_tone5:': '✊ðŸ¿', ':man_biking:': '🚴â€â™‚ï¸', ':man_bowing:': '🙇â€â™‚ï¸', ':man_cartwheeling:': '🤸â€â™‚ï¸', ':man_climbing:': '🧗â€â™‚ï¸', ':man_construction_worker:': '👷â€â™‚ï¸', ':man_detective:': '🕵ï¸â€â™‚ï¸', ':man_elf:': 'ðŸ§â€â™‚ï¸', ':man_facepalming:': '🤦â€â™‚ï¸', ':man_fairy:': '🧚â€â™‚ï¸', ':man_frowning:': 'ðŸ™â€â™‚ï¸', ':man_genie:': '🧞â€â™‚ï¸', ':man_gesturing_no:': '🙅â€â™‚ï¸', ':man_gesturing_ok:': '🙆â€â™‚ï¸', ':man_getting_face_massage:': '💆â€â™‚ï¸', ':man_getting_haircut:': '💇â€â™‚ï¸', ':man_golfing:': 'ðŸŒï¸â€â™‚ï¸', ':man_guard:': '💂â€â™‚ï¸', ':man_health_worker:': '👨â€âš•ï¸', ':man_in_lotus_position:': '🧘â€â™‚ï¸', ':man_in_steamy_room:': '🧖â€â™‚ï¸', ':man_judge:': '👨â€âš–ï¸', ':man_juggling:': '🤹â€â™‚ï¸', ':man_kneeling:': '🧎â€â™‚ï¸', ':man_lifting_weights:': 'ðŸ‹ï¸â€â™‚ï¸', ':man_mage:': '🧙â€â™‚ï¸', ':man_mountain_biking:': '🚵â€â™‚ï¸', ':man_pilot:': '👨â€âœˆï¸', ':man_playing_handball:': '🤾â€â™‚ï¸', ':man_playing_water_polo:': '🤽â€â™‚ï¸', ':man_police_officer:': '👮â€â™‚ï¸', ':man_pouting:': '🙎â€â™‚ï¸', ':man_raising_hand:': '🙋â€â™‚ï¸', ':man_rowing_boat:': '🚣â€â™‚ï¸', ':man_running:': 'ðŸƒâ€â™‚ï¸', ':man_shrugging:': '🤷â€â™‚ï¸', ':man_standing:': 'ðŸ§â€â™‚ï¸', ':man_superhero:': '🦸â€â™‚ï¸', ':man_supervillain:': '🦹â€â™‚ï¸', ':man_surfing:': 'ðŸ„â€â™‚ï¸', ':man_swimming:': 'ðŸŠâ€â™‚ï¸', ':man_tipping_hand:': 'ðŸ’â€â™‚ï¸', ':man_vampire:': '🧛â€â™‚ï¸', ':man_walking:': '🚶â€â™‚ï¸', ':man_wearing_turban:': '👳â€â™‚ï¸', ':man_zombie:': '🧟â€â™‚ï¸', ':men_with_bunny_ears_partying:': '👯â€â™‚ï¸', ':men_wrestling:': '🤼♂ï¸', ':mermaid:': '🧜â€â™€ï¸', ':merman:': '🧜â€â™‚ï¸', ':basketball_player_tone1:': '⛹ï¸ðŸ»', ':person_with_ball_tone1:': '⛹ï¸ðŸ»', ':person_bouncing_ball_tone1:': '⛹ï¸ðŸ»', ':basketball_player_tone2:': '⛹ï¸ðŸ¼', ':person_with_ball_tone2:': '⛹ï¸ðŸ¼', ':person_bouncing_ball_tone2:': '⛹ï¸ðŸ¼', ':basketball_player_tone3:': '⛹ï¸ðŸ½', ':person_with_ball_tone3:': '⛹ï¸ðŸ½', ':person_bouncing_ball_tone3:': '⛹ï¸ðŸ½', ':basketball_player_tone4:': '⛹ï¸ðŸ¾', ':person_with_ball_tone4:': '⛹ï¸ðŸ¾', ':person_bouncing_ball_tone4:': '⛹ï¸ðŸ¾', ':basketball_player_tone5:': '⛹ï¸ðŸ¿', ':person_with_ball_tone5:': '⛹ï¸ðŸ¿', ':person_bouncing_ball_tone5:': '⛹ï¸ðŸ¿', ':pirate_flag:': 'ðŸ´â€â˜ ï¸', ':point_up_tone1:': 'â˜ï¸ðŸ»', ':point_up_tone2:': 'â˜ï¸ðŸ¼', ':point_up_tone3:': 'â˜ï¸ðŸ½', ':point_up_tone4:': 'â˜ï¸ðŸ¾', ':point_up_tone5:': 'â˜ï¸ðŸ¿', ':raised_hand_tone1:': '✋ðŸ»', ':raised_hand_tone2:': '✋ðŸ¼', ':raised_hand_tone3:': '✋ðŸ½', ':raised_hand_tone4:': '✋ðŸ¾', ':raised_hand_tone5:': '✋ðŸ¿', ':v_tone1:': '✌ï¸ðŸ»', ':v_tone2:': '✌ï¸ðŸ¼', ':v_tone3:': '✌ï¸ðŸ½', ':v_tone4:': '✌ï¸ðŸ¾', ':v_tone5:': '✌ï¸ðŸ¿', ':woman_biking:': '🚴â€â™€ï¸', ':woman_bowing:': '🙇â€â™€ï¸', ':woman_cartwheeling:': '🤸â€â™€ï¸', ':woman_climbing:': '🧗â€â™€ï¸', ':woman_construction_worker:': '👷â€â™€ï¸', ':woman_detective:': '🕵ï¸â€â™€ï¸', ':woman_elf:': 'ðŸ§â€â™€ï¸', ':woman_facepalming:': '🤦â€â™€ï¸', ':woman_fairy:': '🧚â€â™€ï¸', ':woman_frowning:': 'ðŸ™â€â™€ï¸', ':woman_genie:': '🧞â€â™€ï¸', ':woman_gesturing_no:': '🙅â€â™€ï¸', ':woman_gesturing_ok:': '🙆â€â™€ï¸', ':woman_getting_face_massage:': '💆â€â™€ï¸', ':woman_getting_haircut:': '💇â€â™€ï¸', ':woman_golfing:': 'ðŸŒï¸â€â™€ï¸', ':woman_guard:': '💂â€â™€ï¸', ':woman_health_worker:': '👩â€âš•ï¸', ':woman_in_lotus_position:': '🧘â€â™€ï¸', ':woman_in_steamy_room:': '🧖â€â™€ï¸', ':woman_judge:': '👩â€âš–ï¸', ':woman_juggling:': '🤹â€â™€ï¸', ':woman_kneeling:': '🧎â€â™€ï¸', ':woman_lifting_weights:': 'ðŸ‹ï¸â€â™€ï¸', ':woman_mage:': '🧙â€â™€ï¸', ':woman_mountain_biking:': '🚵â€â™€ï¸', ':woman_pilot:': '👩â€âœˆï¸', ':woman_playing_handball:': '🤾â€â™€ï¸', ':woman_playing_water_polo:': '🤽â€â™€ï¸', ':woman_police_officer:': '👮â€â™€ï¸', ':woman_pouting:': '🙎â€â™€ï¸', ':woman_raising_hand:': '🙋â€â™€ï¸', ':woman_rowing_boat:': '🚣â€â™€ï¸', ':woman_running:': 'ðŸƒâ€â™€ï¸', ':woman_shrugging:': '🤷â€â™€ï¸', ':woman_standing:': 'ðŸ§â€â™€ï¸', ':woman_superhero:': '🦸â€â™€ï¸', ':woman_supervillain:': '🦹â€â™€ï¸', ':woman_surfing:': 'ðŸ„â€â™€ï¸', ':woman_swimming:': 'ðŸŠâ€â™€ï¸', ':woman_tipping_hand:': 'ðŸ’â€â™€ï¸', ':woman_vampire:': '🧛â€â™€ï¸', ':woman_walking:': '🚶â€â™€ï¸', ':woman_wearing_turban:': '👳â€â™€ï¸', ':woman_zombie:': '🧟â€â™€ï¸', ':women_with_bunny_ears_partying:': '👯â€â™€ï¸', ':women_wrestling:': '🤼♀ï¸', ':writing_hand_tone1:': 'âœï¸ðŸ»', ':writing_hand_tone2:': 'âœï¸ðŸ¼', ':writing_hand_tone3:': 'âœï¸ðŸ½', ':writing_hand_tone4:': 'âœï¸ðŸ¾', ':writing_hand_tone5:': 'âœï¸ðŸ¿', ':keycap_asterisk:': '*ï¸âƒ£', ':asterisk:': '*ï¸âƒ£', ':eight:': '8ï¸âƒ£', ':five:': '5ï¸âƒ£', ':four:': '4ï¸âƒ£', ':hash:': '#ï¸âƒ£', ':man_bouncing_ball:': '⛹ï¸â€â™‚ï¸', ':nine:': '9ï¸âƒ£', ':one:': '1ï¸âƒ£', ':seven:': '7ï¸âƒ£', ':six:': '6ï¸âƒ£', ':three:': '3ï¸âƒ£', ':two:': '2ï¸âƒ£', ':woman_bouncing_ball:': '⛹ï¸â€â™€ï¸', ':zero:': '0ï¸âƒ£', ':100:': '💯', ':1234:': '🔢', ':8ball:': '🎱', ':a:': '🅰ï¸', ':ab:': '🆎', ':abacus:': '🧮', ':abc:': '🔤', ':abcd:': '🔡', ':accept:': '🉑', ':adhesive_bandage:': '🩹', ':adult:': '🧑', ':aerial_tramway:': '🚡', ':airplane_arriving:': '🛬', ':airplane_departure:': '🛫', ':small_airplane:': '🛩ï¸', ':airplane_small:': '🛩ï¸', ':alien:': '👽', ':ambulance:': '🚑', ':amphora:': 'ðŸº', ':angel:': '👼', ':anger:': '💢', ':right_anger_bubble:': '🗯ï¸', ':anger_right:': '🗯ï¸', ':angry:': '😠', ':anguished:': '😧', ':ant:': 'ðŸœ', ':apple:': 'ðŸŽ', ':arrow_down_small:': '🔽', ':arrow_up_small:': '🔼', ':arrows_clockwise:': '🔃', ':arrows_counterclockwise:': '🔄', ':art:': '🎨', ':articulated_lorry:': '🚛', ':astonished:': '😲', ':athletic_shoe:': '👟', ':atm:': 'ðŸ§', ':auto_rickshaw:': '🛺', ':avocado:': '🥑', ':axe:': '🪓', ':b:': '🅱ï¸', ':baby:': '👶', ':baby_bottle:': 'ðŸ¼', ':baby_chick:': 'ðŸ¤', ':baby_symbol:': '🚼', ':back:': '🔙', ':bacon:': '🥓', ':badger:': '🦡', ':badminton:': 'ðŸ¸', ':bagel:': '🥯', ':baggage_claim:': '🛄', ':bald:': '🦲', ':ballet_shoes:': '🩰', ':balloon:': '🎈', ':ballot_box_with_ballot:': '🗳ï¸', ':ballot_box:': '🗳ï¸', ':bamboo:': 'ðŸŽ', ':banana:': 'ðŸŒ', ':banjo:': '🪕', ':bank:': 'ðŸ¦', ':bar_chart:': '📊', ':barber:': '💈', ':basket:': '🧺', ':basketball:': 'ðŸ€', ':bat:': '🦇', ':bath:': '🛀', ':bathtub:': 'ðŸ›', ':battery:': '🔋', ':beach_with_umbrella:': 'ðŸ–ï¸', ':beach:': 'ðŸ–ï¸', ':bear:': 'ðŸ»', ':bearded_person:': '🧔', ':bed:': 'ðŸ›ï¸', ':bee:': 'ðŸ', ':beer:': 'ðŸº', ':beers:': 'ðŸ»', ':beetle:': 'ðŸž', ':beginner:': '🔰', ':bell:': '🔔', ':bellhop_bell:': '🛎ï¸', ':bellhop:': '🛎ï¸', ':bento:': 'ðŸ±', ':beverage_box:': '🧃', ':bike:': '🚲', ':bikini:': '👙', ':billed_cap:': '🧢', ':bird:': 'ðŸ¦', ':birthday:': '🎂', ':black_heart:': '🖤', ':black_joker:': 'ðŸƒ', ':black_square_button:': '🔲', ':person_with_blond_hair:': '👱', ':blond_haired_person:': '👱', ':blossom:': '🌼', ':blowfish:': 'ðŸ¡', ':blue_book:': '📘', ':blue_car:': '🚙', ':blue_circle:': '🔵', ':blue_heart:': '💙', ':blue_square:': '🟦', ':blush:': '😊', ':boar:': 'ðŸ—', ':bomb:': '💣', ':bone:': '🦴', ':book:': '📖', ':bookmark:': '🔖', ':bookmark_tabs:': '📑', ':books:': '📚', ':boom:': '💥', ':boot:': '👢', ':bouquet:': 'ðŸ’', ':archery:': 'ðŸ¹', ':bow_and_arrow:': 'ðŸ¹', ':bowl_with_spoon:': '🥣', ':bowling:': '🎳', ':boxing_gloves:': '🥊', ':boxing_glove:': '🥊', ':boy:': '👦', ':brain:': '🧠', ':bread:': 'ðŸž', ':breast_feeding:': '🤱', ':bricks:': '🧱', ':bride_with_veil:': '👰', ':bridge_at_night:': '🌉', ':briefcase:': '💼', ':briefs:': '🩲', ':broccoli:': '🥦', ':broken_heart:': '💔', ':broom:': '🧹', ':brown_circle:': '🟤', ':brown_heart:': '🤎', ':brown_square:': '🟫', ':bug:': 'ðŸ›', ':bulb:': '💡', ':bullettrain_front:': '🚅', ':bullettrain_side:': '🚄', ':burrito:': '🌯', ':bus:': '🚌', ':busstop:': 'ðŸš', ':bust_in_silhouette:': '👤', ':busts_in_silhouette:': '👥', ':butter:': '🧈', ':butterfly:': '🦋', ':cactus:': '🌵', ':cake:': 'ðŸ°', ':calendar:': '📆', ':spiral_calendar_pad:': '🗓ï¸', ':calendar_spiral:': '🗓ï¸', ':call_me_hand:': '🤙', ':call_me:': '🤙', ':calling:': '📲', ':camel:': 'ðŸ«', ':camera:': '📷', ':camera_with_flash:': '📸', ':camping:': 'ðŸ•ï¸', ':candle:': '🕯ï¸', ':candy:': 'ðŸ¬', ':canned_food:': '🥫', ':kayak:': '🛶', ':canoe:': '🛶', ':capital_abcd:': '🔠', ':card_file_box:': '🗃ï¸', ':card_box:': '🗃ï¸', ':card_index:': '📇', ':carousel_horse:': '🎠', ':carrot:': '🥕', ':cat2:': 'ðŸˆ', ':cat:': 'ðŸ±', ':cd:': '💿', ':chair:': '🪑', ':bottle_with_popping_cork:': 'ðŸ¾', ':champagne:': 'ðŸ¾', ':clinking_glass:': '🥂', ':champagne_glass:': '🥂', ':chart:': '💹', ':chart_with_downwards_trend:': '📉', ':chart_with_upwards_trend:': '📈', ':checkered_flag:': 'ðŸ', ':cheese_wedge:': '🧀', ':cheese:': '🧀', ':cherries:': 'ðŸ’', ':cherry_blossom:': '🌸', ':chestnut:': '🌰', ':chicken:': 'ðŸ”', ':child:': '🧒', ':children_crossing:': '🚸', ':chipmunk:': 'ðŸ¿ï¸', ':chocolate_bar:': 'ðŸ«', ':chopsticks:': '🥢', ':christmas_tree:': '🎄', ':cinema:': '🎦', ':circus_tent:': '🎪', ':city_dusk:': '🌆', ':city_sunrise:': '🌇', ':city_sunset:': '🌇', ':cityscape:': 'ðŸ™ï¸', ':cl:': '🆑', ':clap:': 'ðŸ‘', ':clapper:': '🎬', ':classical_building:': 'ðŸ›ï¸', ':clipboard:': '📋', ':clock1030:': '🕥', ':clock10:': '🕙', ':clock1130:': '🕦', ':clock11:': '🕚', ':clock1230:': '🕧', ':clock12:': '🕛', ':clock130:': '🕜', ':clock1:': 'ðŸ•', ':clock230:': 'ðŸ•', ':clock2:': '🕑', ':clock330:': '🕞', ':clock3:': '🕒', ':clock430:': '🕟', ':clock4:': '🕓', ':clock530:': '🕠', ':clock5:': '🕔', ':clock630:': '🕡', ':clock6:': '🕕', ':clock730:': '🕢', ':clock7:': '🕖', ':clock830:': '🕣', ':clock8:': '🕗', ':clock930:': '🕤', ':clock9:': '🕘', ':mantlepiece_clock:': '🕰ï¸', ':clock:': '🕰ï¸', ':closed_book:': '📕', ':closed_lock_with_key:': 'ðŸ”', ':closed_umbrella:': '🌂', ':cloud_with_lightning:': '🌩ï¸', ':cloud_lightning:': '🌩ï¸', ':cloud_with_rain:': '🌧ï¸', ':cloud_rain:': '🌧ï¸', ':cloud_with_snow:': '🌨ï¸', ':cloud_snow:': '🌨ï¸', ':cloud_with_tornado:': '🌪ï¸', ':cloud_tornado:': '🌪ï¸', ':clown_face:': '🤡', ':clown:': '🤡', ':coat:': '🧥', ':cocktail:': 'ðŸ¸', ':coconut:': '🥥', ':cold_face:': '🥶', ':cold_sweat:': '😰', ':compass:': 'ðŸ§', ':compression:': '🗜ï¸', ':computer:': '💻', ':confetti_ball:': '🎊', ':confounded:': '😖', ':confused:': '😕', ':construction:': '🚧', ':building_construction:': 'ðŸ—ï¸', ':construction_site:': 'ðŸ—ï¸', ':construction_worker:': '👷', ':control_knobs:': '🎛ï¸', ':convenience_store:': 'ðŸª', ':cookie:': 'ðŸª', ':cooking:': 'ðŸ³', ':cool:': '🆒', ':corn:': '🌽', ':couch_and_lamp:': '🛋ï¸', ':couch:': '🛋ï¸', ':couple:': '👫', ':couple_with_heart:': '💑', ':couplekiss:': 'ðŸ’', ':cow2:': 'ðŸ„', ':cow:': 'ðŸ®', ':face_with_cowboy_hat:': '🤠', ':cowboy:': '🤠', ':crab:': '🦀', ':lower_left_crayon:': 'ðŸ–ï¸', ':crayon:': 'ðŸ–ï¸', ':credit_card:': '💳', ':crescent_moon:': '🌙', ':cricket:': '🦗', ':cricket_bat_ball:': 'ðŸ', ':cricket_game:': 'ðŸ', ':crocodile:': 'ðŸŠ', ':croissant:': 'ðŸ¥', ':crossed_flags:': '🎌', ':crown:': '👑', ':passenger_ship:': '🛳ï¸', ':cruise_ship:': '🛳ï¸', ':cry:': '😢', ':crying_cat_face:': '😿', ':crystal_ball:': '🔮', ':cucumber:': '🥒', ':cup_with_straw:': '🥤', ':cupcake:': 'ðŸ§', ':cupid:': '💘', ':curling_stone:': '🥌', ':curly_haired:': '🦱', ':currency_exchange:': '💱', ':curry:': 'ðŸ›', ':pudding:': 'ðŸ®', ':flan:': 'ðŸ®', ':custard:': 'ðŸ®', ':customs:': '🛃', ':cut_of_meat:': '🥩', ':cyclone:': '🌀', ':dagger_knife:': '🗡ï¸', ':dagger:': '🗡ï¸', ':dancer:': '💃', ':dango:': 'ðŸ¡', ':dark_sunglasses:': '🕶ï¸', ':dart:': '🎯', ':dash:': '💨', ':date:': '📅', ':deaf_person:': 'ðŸ§', ':deciduous_tree:': '🌳', ':deer:': '🦌', ':department_store:': 'ðŸ¬', ':desert:': 'ðŸœï¸', ':desktop_computer:': '🖥ï¸', ':desktop:': '🖥ï¸', ':spy:': '🕵ï¸', ':sleuth_or_spy:': '🕵ï¸', ':detective:': '🕵ï¸', ':diamond_shape_with_a_dot_inside:': '💠', ':disappointed:': '😞', ':disappointed_relieved:': '😥', ':card_index_dividers:': '🗂ï¸', ':dividers:': '🗂ï¸', ':diving_mask:': '🤿', ':diya_lamp:': '🪔', ':dizzy:': '💫', ':dizzy_face:': '😵', ':dna:': '🧬', ':do_not_litter:': '🚯', ':dog2:': 'ðŸ•', ':dog:': 'ðŸ¶', ':dollar:': '💵', ':dolls:': '🎎', ':dolphin:': 'ðŸ¬', ':door:': '🚪', ':doughnut:': 'ðŸ©', ':dove_of_peace:': '🕊ï¸', ':dove:': '🕊ï¸', ':dragon:': 'ðŸ‰', ':dragon_face:': 'ðŸ²', ':dress:': '👗', ':dromedary_camel:': 'ðŸª', ':drool:': '🤤', ':drooling_face:': '🤤', ':drop_of_blood:': '🩸', ':droplet:': '💧', ':drum_with_drumsticks:': 'ðŸ¥', ':drum:': 'ðŸ¥', ':duck:': '🦆', ':dumpling:': '🥟', ':dvd:': '📀', ':email:': '📧', ':e-mail:': '📧', ':eagle:': '🦅', ':ear:': '👂', ':ear_of_rice:': '🌾', ':ear_with_hearing_aid:': '🦻', ':earth_africa:': 'ðŸŒ', ':earth_americas:': '🌎', ':earth_asia:': 'ðŸŒ', ':egg:': '🥚', ':eggplant:': 'ðŸ†', ':electric_plug:': '🔌', ':elephant:': 'ðŸ˜', ':elf:': 'ðŸ§', ':end:': '🔚', ':envelope_with_arrow:': '📩', ':euro:': '💶', ':european_castle:': 'ðŸ°', ':european_post_office:': 'ðŸ¤', ':evergreen_tree:': '🌲', ':exploding_head:': '🤯', ':expressionless:': '😑', ':eye:': 'ðŸ‘ï¸', ':eyeglasses:': '👓', ':eyes:': '👀', ':face_vomiting:': '🤮', ':face_with_hand_over_mouth:': 'ðŸ¤', ':face_with_monocle:': 'ðŸ§', ':face_with_raised_eyebrow:': '🤨', ':face_with_symbols_over_mouth:': '🤬', ':factory:': 'ðŸ', ':fairy:': '🧚', ':falafel:': '🧆', ':fallen_leaf:': 'ðŸ‚', ':family:': '👪', ':fax:': '📠', ':fearful:': '😨', ':paw_prints:': 'ðŸ¾', ':feet:': 'ðŸ¾', ':ferris_wheel:': '🎡', ':field_hockey:': 'ðŸ‘', ':file_cabinet:': '🗄ï¸', ':file_folder:': 'ðŸ“', ':film_frames:': '🎞ï¸', ':hand_with_index_and_middle_finger_crossed:': '🤞', ':fingers_crossed:': '🤞', ':flame:': '🔥', ':fire:': '🔥', ':fire_engine:': '🚒', ':fire_extinguisher:': '🧯', ':firecracker:': '🧨', ':fireworks:': '🎆', ':first_place_medal:': '🥇', ':first_place:': '🥇', ':first_quarter_moon:': '🌓', ':first_quarter_moon_with_face:': '🌛', ':fish:': 'ðŸŸ', ':fish_cake:': 'ðŸ¥', ':fishing_pole_and_fish:': '🎣', ':waving_black_flag:': 'ðŸ´', ':flag_black:': 'ðŸ´', ':waving_white_flag:': 'ðŸ³ï¸', ':flag_white:': 'ðŸ³ï¸', ':flags:': 'ðŸŽ', ':flamingo:': '🦩', ':flashlight:': '🔦', ':floppy_disk:': '💾', ':flower_playing_cards:': '🎴', ':flushed:': '😳', ':flying_disc:': 'ðŸ¥', ':flying_saucer:': '🛸', ':fog:': '🌫ï¸', ':foggy:': 'ðŸŒ', ':foot:': '🦶', ':football:': 'ðŸˆ', ':footprints:': '👣', ':fork_and_knife:': 'ðŸ´', ':fork_and_knife_with_plate:': 'ðŸ½ï¸', ':fork_knife_plate:': 'ðŸ½ï¸', ':fortune_cookie:': '🥠', ':four_leaf_clover:': 'ðŸ€', ':fox_face:': '🦊', ':fox:': '🦊', ':frame_with_picture:': '🖼ï¸', ':frame_photo:': '🖼ï¸', ':free:': '🆓', ':baguette_bread:': '🥖', ':french_bread:': '🥖', ':fried_shrimp:': 'ðŸ¤', ':fries:': 'ðŸŸ', ':frog:': 'ðŸ¸', ':frowning:': '😦', ':full_moon:': '🌕', ':full_moon_with_face:': 'ðŸŒ', ':game_die:': '🎲', ':garlic:': '🧄', ':gem:': '💎', ':genie:': '🧞', ':ghost:': '👻', ':gift:': 'ðŸŽ', ':gift_heart:': 'ðŸ’', ':giraffe:': '🦒', ':girl:': '👧', ':globe_with_meridians:': 'ðŸŒ', ':gloves:': '🧤', ':goal_net:': '🥅', ':goal:': '🥅', ':goat:': 'ðŸ', ':goggles:': '🥽', ':gorilla:': 'ðŸ¦', ':grapes:': 'ðŸ‡', ':green_apple:': 'ðŸ', ':green_book:': '📗', ':green_circle:': '🟢', ':green_heart:': '💚', ':green_square:': '🟩', ':grimacing:': '😬', ':grin:': 'ðŸ˜', ':grinning:': '😀', ':guardsman:': '💂', ':guard:': '💂', ':guide_dog:': '🦮', ':guitar:': '🎸', ':gun:': '🔫', ':hamburger:': 'ðŸ”', ':hammer:': '🔨', ':hamster:': 'ðŸ¹', ':raised_hand_with_fingers_splayed:': 'ðŸ–ï¸', ':hand_splayed:': 'ðŸ–ï¸', ':handbag:': '👜', ':shaking_hands:': 'ðŸ¤', ':handshake:': 'ðŸ¤', ':hatched_chick:': 'ðŸ¥', ':hatching_chick:': 'ðŸ£', ':face_with_head_bandage:': '🤕', ':head_bandage:': '🤕', ':headphones:': '🎧', ':hear_no_evil:': '🙉', ':heart_decoration:': '💟', ':heart_eyes:': 'ðŸ˜', ':heart_eyes_cat:': '😻', ':heartbeat:': '💓', ':heartpulse:': '💗', ':heavy_dollar_sign:': '💲', ':hedgehog:': '🦔', ':helicopter:': 'ðŸš', ':herb:': '🌿', ':hibiscus:': '🌺', ':high_brightness:': '🔆', ':high_heel:': '👠', ':hiking_boot:': '🥾', ':hindu_temple:': '🛕', ':hippopotamus:': '🦛', ':hockey:': 'ðŸ’', ':hole:': '🕳ï¸', ':house_buildings:': 'ðŸ˜ï¸', ':homes:': 'ðŸ˜ï¸', ':honey_pot:': 'ðŸ¯', ':horse:': 'ðŸ´', ':horse_racing:': 'ðŸ‡', ':hospital:': 'ðŸ¥', ':hot_face:': '🥵', ':hot_pepper:': '🌶ï¸', ':hot_dog:': 'ðŸŒ', ':hotdog:': 'ðŸŒ', ':hotel:': 'ðŸ¨', ':house:': 'ðŸ ', ':derelict_house_building:': 'ðŸšï¸', ':house_abandoned:': 'ðŸšï¸', ':house_with_garden:': 'ðŸ¡', ':hugging_face:': '🤗', ':hugging:': '🤗', ':hushed:': '😯', ':ice_cream:': 'ðŸ¨', ':ice_cube:': '🧊', ':icecream:': 'ðŸ¦', ':id:': '🆔', ':ideograph_advantage:': 'ðŸ‰', ':imp:': '👿', ':inbox_tray:': '📥', ':incoming_envelope:': '📨', ':innocent:': '😇', ':iphone:': '📱', ':desert_island:': 'ðŸï¸', ':island:': 'ðŸï¸', ':izakaya_lantern:': 'ðŸ®', ':jack_o_lantern:': '🎃', ':japan:': '🗾', ':japanese_castle:': 'ðŸ¯', ':japanese_goblin:': '👺', ':japanese_ogre:': '👹', ':jeans:': '👖', ':jigsaw:': '🧩', ':joy:': '😂', ':joy_cat:': '😹', ':joystick:': '🕹ï¸', ':kaaba:': '🕋', ':kangaroo:': '🦘', ':old_key:': 'ðŸ—ï¸', ':key2:': 'ðŸ—ï¸', ':key:': '🔑', ':keycap_ten:': '🔟', ':kimono:': '👘', ':kiss:': '💋', ':kissing:': '😗', ':kissing_cat:': '😽', ':kissing_closed_eyes:': '😚', ':kissing_heart:': '😘', ':kissing_smiling_eyes:': '😙', ':kite:': 'ðŸª', ':kiwifruit:': 'ðŸ¥', ':kiwi:': 'ðŸ¥', ':knife:': '🔪', ':koala:': 'ðŸ¨', ':koko:': 'ðŸˆ', ':lab_coat:': '🥼', ':label:': 'ðŸ·ï¸', ':lacrosse:': 'ðŸ¥', ':large_blue_diamond:': '🔷', ':large_orange_diamond:': '🔶', ':last_quarter_moon:': '🌗', ':last_quarter_moon_with_face:': '🌜', ':satisfied:': '😆', ':laughing:': '😆', ':leafy_green:': '🥬', ':leaves:': 'ðŸƒ', ':ledger:': '📒', ':left_fist:': '🤛', ':left_facing_fist:': '🤛', ':left_luggage:': '🛅', ':leg:': '🦵', ':lemon:': 'ðŸ‹', ':leopard:': 'ðŸ†', ':level_slider:': '🎚ï¸', ':man_in_business_suit_levitating:': '🕴ï¸', ':levitate:': '🕴ï¸', ':light_rail:': '🚈', ':link:': '🔗', ':lion:': 'ðŸ¦', ':lion_face:': 'ðŸ¦', ':lips:': '👄', ':lipstick:': '💄', ':lizard:': '🦎', ':llama:': '🦙', ':lobster:': '🦞', ':lock:': '🔒', ':lock_with_ink_pen:': 'ðŸ”', ':lollipop:': 'ðŸ', ':loud_sound:': '🔊', ':loudspeaker:': '📢', ':love_hotel:': 'ðŸ©', ':love_letter:': '💌', ':love_you_gesture:': '🤟', ':low_brightness:': '🔅', ':luggage:': '🧳', ':liar:': '🤥', ':lying_face:': '🤥', ':mag:': 'ðŸ”', ':mag_right:': '🔎', ':mage:': '🧙', ':magnet:': '🧲', ':mahjong:': '🀄', ':mailbox:': '📫', ':mailbox_closed:': '📪', ':mailbox_with_mail:': '📬', ':mailbox_with_no_mail:': 'ðŸ“', ':man:': '👨', ':male_dancer:': '🕺', ':man_dancing:': '🕺', ':man_in_tuxedo:': '🤵', ':man_with_gua_pi_mao:': '👲', ':man_with_chinese_cap:': '👲', ':mango:': 'ðŸ¥', ':mans_shoe:': '👞', ':manual_wheelchair:': '🦽', ':world_map:': '🗺ï¸', ':map:': '🗺ï¸', ':maple_leaf:': 'ðŸ', ':karate_uniform:': '🥋', ':martial_arts_uniform:': '🥋', ':mask:': '😷', ':mate:': '🧉', ':meat_on_bone:': 'ðŸ–', ':mechanical_arm:': '🦾', ':mechanical_leg:': '🦿', ':sports_medal:': 'ðŸ…', ':medal:': 'ðŸ…', ':mega:': '📣', ':melon:': 'ðŸˆ', ':menorah:': '🕎', ':mens:': '🚹', ':merperson:': '🧜', ':sign_of_the_horns:': '🤘', ':metal:': '🤘', ':metro:': '🚇', ':microbe:': '🦠', ':studio_microphone:': '🎙ï¸', ':microphone2:': '🎙ï¸', ':microphone:': '🎤', ':microscope:': '🔬', ':reversed_hand_with_middle_finger_extended:': '🖕', ':middle_finger:': '🖕', ':military_medal:': '🎖ï¸', ':glass_of_milk:': '🥛', ':milk:': '🥛', ':milky_way:': '🌌', ':minibus:': 'ðŸš', ':minidisc:': '💽', ':mobile_phone_off:': '📴', ':money_mouth_face:': '🤑', ':money_mouth:': '🤑', ':money_with_wings:': '💸', ':moneybag:': '💰', ':monkey:': 'ðŸ’', ':monkey_face:': 'ðŸµ', ':monorail:': 'ðŸš', ':moon_cake:': '🥮', ':mortar_board:': '🎓', ':mosque:': '🕌', ':mosquito:': '🦟', ':motorbike:': '🛵', ':motor_scooter:': '🛵', ':motorboat:': '🛥ï¸', ':racing_motorcycle:': 'ðŸï¸', ':motorcycle:': 'ðŸï¸', ':motorized_wheelchair:': '🦼', ':motorway:': '🛣ï¸', ':mount_fuji:': '🗻', ':mountain_cableway:': '🚠', ':mountain_railway:': '🚞', ':snow_capped_mountain:': 'ðŸ”ï¸', ':mountain_snow:': 'ðŸ”ï¸', ':mouse2:': 'ðŸ', ':mouse:': 'ðŸ', ':three_button_mouse:': '🖱ï¸', ':mouse_three_button:': '🖱ï¸', ':movie_camera:': '🎥', ':moyai:': '🗿', ':mother_christmas:': '🤶', ':mrs_claus:': '🤶', ':muscle:': '💪', ':mushroom:': 'ðŸ„', ':musical_keyboard:': '🎹', ':musical_note:': '🎵', ':musical_score:': '🎼', ':mute:': '🔇', ':nail_care:': '💅', ':name_badge:': '📛', ':sick:': '🤢', ':nauseated_face:': '🤢', ':nazar_amulet:': '🧿', ':necktie:': '👔', ':nerd_face:': '🤓', ':nerd:': '🤓', ':neutral_face:': 'ðŸ˜', ':new:': '🆕', ':new_moon:': '🌑', ':new_moon_with_face:': '🌚', ':rolled_up_newspaper:': '🗞ï¸', ':newspaper2:': '🗞ï¸', ':newspaper:': '📰', ':ng:': '🆖', ':night_with_stars:': '🌃', ':no_bell:': '🔕', ':no_bicycles:': '🚳', ':no_entry_sign:': '🚫', ':no_mobile_phones:': '📵', ':no_mouth:': '😶', ':no_pedestrians:': '🚷', ':no_smoking:': 'ðŸš', ':non-potable_water:': '🚱', ':nose:': '👃', ':notebook:': '📓', ':notebook_with_decorative_cover:': '📔', ':spiral_note_pad:': '🗒ï¸', ':notepad_spiral:': '🗒ï¸', ':notes:': '🎶', ':nut_and_bolt:': '🔩', ':o2:': '🅾ï¸', ':ocean:': '🌊', ':stop_sign:': '🛑', ':octagonal_sign:': '🛑', ':octopus:': 'ðŸ™', ':oden:': 'ðŸ¢', ':office:': 'ðŸ¢', ':oil_drum:': '🛢ï¸', ':oil:': '🛢ï¸', ':ok:': '🆗', ':ok_hand:': '👌', ':older_adult:': '🧓', ':older_man:': '👴', ':grandma:': '👵', ':older_woman:': '👵', ':om_symbol:': '🕉ï¸', ':on:': '🔛', ':oncoming_automobile:': '🚘', ':oncoming_bus:': 'ðŸš', ':oncoming_police_car:': '🚔', ':oncoming_taxi:': '🚖', ':one_piece_swimsuit:': '🩱', ':onion:': '🧅', ':open_file_folder:': '📂', ':open_hands:': 'ðŸ‘', ':open_mouth:': '😮', ':orange_book:': '📙', ':orange_circle:': '🟠', ':orange_heart:': '🧡', ':orange_square:': '🟧', ':orangutan:': '🦧', ':otter:': '🦦', ':outbox_tray:': '📤', ':owl:': '🦉', ':ox:': 'ðŸ‚', ':oyster:': '🦪', ':package:': '📦', ':page_facing_up:': '📄', ':page_with_curl:': '📃', ':pager:': '📟', ':lower_left_paintbrush:': '🖌ï¸', ':paintbrush:': '🖌ï¸', ':palm_tree:': '🌴', ':palms_up_together:': '🤲', ':pancakes:': '🥞', ':panda_face:': 'ðŸ¼', ':paperclip:': '📎', ':linked_paperclips:': '🖇ï¸', ':paperclips:': '🖇ï¸', ':parachute:': '🪂', ':national_park:': 'ðŸžï¸', ':park:': 'ðŸžï¸', ':parking:': '🅿ï¸', ':parrot:': '🦜', ':partying_face:': '🥳', ':passport_control:': '🛂', ':peach:': 'ðŸ‘', ':peacock:': '🦚', ':shelled_peanut:': '🥜', ':peanuts:': '🥜', ':pear:': 'ðŸ', ':lower_left_ballpoint_pen:': '🖊ï¸', ':pen_ballpoint:': '🖊ï¸', ':lower_left_fountain_pen:': '🖋ï¸', ':pen_fountain:': '🖋ï¸', ':memo:': 'ðŸ“', ':pencil:': 'ðŸ“', ':penguin:': 'ðŸ§', ':pensive:': '😔', ':dancers:': '👯', ':people_with_bunny_ears_partying:': '👯', ':wrestlers:': '🤼', ':wrestling:': '🤼', ':people_wrestling:': '🤼', ':performing_arts:': 'ðŸŽ', ':persevere:': '😣', ':bicyclist:': '🚴', ':person_biking:': '🚴', ':bow:': '🙇', ':person_bowing:': '🙇', ':person_climbing:': '🧗', ':cartwheel:': '🤸', ':person_doing_cartwheel:': '🤸', ':face_palm:': '🤦', ':facepalm:': '🤦', ':person_facepalming:': '🤦', ':fencer:': '🤺', ':fencing:': '🤺', ':person_fencing:': '🤺', ':person_frowning:': 'ðŸ™', ':no_good:': '🙅', ':person_gesturing_no:': '🙅', ':ok_woman:': '🙆', ':person_gesturing_ok:': '🙆', ':haircut:': '💇', ':person_getting_haircut:': '💇', ':massage:': '💆', ':person_getting_massage:': '💆', ':golfer:': 'ðŸŒï¸', ':person_golfing:': 'ðŸŒï¸', ':person_in_lotus_position:': '🧘', ':person_in_steamy_room:': '🧖', ':juggling:': '🤹', ':juggler:': '🤹', ':person_juggling:': '🤹', ':person_kneeling:': '🧎', ':lifter:': 'ðŸ‹ï¸', ':weight_lifter:': 'ðŸ‹ï¸', ':person_lifting_weights:': 'ðŸ‹ï¸', ':mountain_bicyclist:': '🚵', ':person_mountain_biking:': '🚵', ':handball:': '🤾', ':person_playing_handball:': '🤾', ':water_polo:': '🤽', ':person_playing_water_polo:': '🤽', ':person_with_pouting_face:': '🙎', ':person_pouting:': '🙎', ':raising_hand:': '🙋', ':person_raising_hand:': '🙋', ':rowboat:': '🚣', ':person_rowing_boat:': '🚣', ':runner:': 'ðŸƒ', ':person_running:': 'ðŸƒ', ':shrug:': '🤷', ':person_shrugging:': '🤷', ':person_standing:': 'ðŸ§', ':surfer:': 'ðŸ„', ':person_surfing:': 'ðŸ„', ':swimmer:': 'ðŸŠ', ':person_swimming:': 'ðŸŠ', ':information_desk_person:': 'ðŸ’', ':person_tipping_hand:': 'ðŸ’', ':walking:': '🚶', ':person_walking:': '🚶', ':man_with_turban:': '👳', ':person_wearing_turban:': '👳', ':petri_dish:': '🧫', ':pie:': '🥧', ':pig2:': 'ðŸ–', ':pig:': 'ðŸ·', ':pig_nose:': 'ðŸ½', ':pill:': '💊', ':pinching_hand:': 'ðŸ¤', ':pineapple:': 'ðŸ', ':table_tennis:': 'ðŸ“', ':ping_pong:': 'ðŸ“', ':pizza:': 'ðŸ•', ':worship_symbol:': 'ðŸ›', ':place_of_worship:': 'ðŸ›', ':pleading_face:': '🥺', ':point_down:': '👇', ':point_left:': '👈', ':point_right:': '👉', ':point_up_2:': '👆', ':police_car:': '🚓', ':cop:': '👮', ':police_officer:': '👮', ':poodle:': 'ðŸ©', ':shit:': '💩', ':hankey:': '💩', ':poo:': '💩', ':poop:': '💩', ':popcorn:': 'ðŸ¿', ':post_office:': 'ðŸ£', ':postal_horn:': '📯', ':postbox:': '📮', ':potable_water:': '🚰', ':potato:': '🥔', ':pouch:': 'ðŸ‘', ':poultry_leg:': 'ðŸ—', ':pound:': '💷', ':pouting_cat:': '😾', ':pray:': 'ðŸ™', ':prayer_beads:': '📿', ':expecting_woman:': '🤰', ':pregnant_woman:': '🤰', ':pretzel:': '🥨', ':prince:': '🤴', ':princess:': '👸', ':printer:': '🖨ï¸', ':probing_cane:': '🦯', ':film_projector:': '📽ï¸', ':projector:': '📽ï¸', ':punch:': '👊', ':purple_circle:': '🟣', ':purple_heart:': '💜', ':purple_square:': '🟪', ':purse:': '👛', ':pushpin:': '📌', ':put_litter_in_its_place:': '🚮', ':rabbit2:': 'ðŸ‡', ':rabbit:': 'ðŸ°', ':raccoon:': 'ðŸ¦', ':racing_car:': 'ðŸŽï¸', ':race_car:': 'ðŸŽï¸', ':racehorse:': 'ðŸŽ', ':radio:': '📻', ':radio_button:': '🔘', ':rage:': '😡', ':railway_car:': '🚃', ':railroad_track:': '🛤ï¸', ':railway_track:': '🛤ï¸', ':rainbow:': '🌈', ':back_of_hand:': '🤚', ':raised_back_of_hand:': '🤚', ':raised_hands:': '🙌', ':ram:': 'ðŸ', ':ramen:': 'ðŸœ', ':rat:': 'ðŸ€', ':razor:': '🪒', ':receipt:': '🧾', ':red_car:': '🚗', ':red_circle:': '🔴', ':red_envelope:': '🧧', ':red_haired:': '🦰', ':red_square:': '🟥', ':regional_indicator_a:': '🇦', ':regional_indicator_b:': '🇧', ':regional_indicator_c:': '🇨', ':regional_indicator_d:': '🇩', ':regional_indicator_e:': '🇪', ':regional_indicator_f:': '🇫', ':regional_indicator_g:': '🇬', ':regional_indicator_h:': 'ðŸ‡', ':regional_indicator_i:': '🇮', ':regional_indicator_j:': '🇯', ':regional_indicator_k:': '🇰', ':regional_indicator_l:': '🇱', ':regional_indicator_m:': '🇲', ':regional_indicator_n:': '🇳', ':regional_indicator_o:': '🇴', ':regional_indicator_p:': '🇵', ':regional_indicator_q:': '🇶', ':regional_indicator_r:': '🇷', ':regional_indicator_s:': '🇸', ':regional_indicator_t:': '🇹', ':regional_indicator_u:': '🇺', ':regional_indicator_v:': '🇻', ':regional_indicator_w:': '🇼', ':regional_indicator_x:': '🇽', ':regional_indicator_y:': '🇾', ':regional_indicator_z:': '🇿', ':relieved:': '😌', ':reminder_ribbon:': '🎗ï¸', ':repeat:': 'ðŸ”', ':repeat_one:': '🔂', ':restroom:': '🚻', ':revolving_hearts:': '💞', ':rhinoceros:': 'ðŸ¦', ':rhino:': 'ðŸ¦', ':ribbon:': '🎀', ':rice:': 'ðŸš', ':rice_ball:': 'ðŸ™', ':rice_cracker:': 'ðŸ˜', ':rice_scene:': '🎑', ':right_fist:': '🤜', ':right_facing_fist:': '🤜', ':ring:': 'ðŸ’', ':ringed_planet:': 'ðŸª', ':robot_face:': '🤖', ':robot:': '🤖', ':rocket:': '🚀', ':rolling_on_the_floor_laughing:': '🤣', ':rofl:': '🤣', ':roll_of_paper:': '🧻', ':roller_coaster:': '🎢', ':face_with_rolling_eyes:': '🙄', ':rolling_eyes:': '🙄', ':rooster:': 'ðŸ“', ':rose:': '🌹', ':rosette:': 'ðŸµï¸', ':rotating_light:': '🚨', ':round_pushpin:': 'ðŸ“', ':rugby_football:': 'ðŸ‰', ':running_shirt_with_sash:': '🎽', ':sa:': '🈂ï¸', ':safety_pin:': '🧷', ':safety_vest:': '🦺', ':sake:': 'ðŸ¶', ':green_salad:': '🥗', ':salad:': '🥗', ':salt:': '🧂', ':sandal:': '👡', ':sandwich:': '🥪', ':santa:': '🎅', ':sari:': '🥻', ':satellite:': '📡', ':satellite_orbital:': '🛰ï¸', ':sauropod:': '🦕', ':saxophone:': '🎷', ':scarf:': '🧣', ':school:': 'ðŸ«', ':school_satchel:': '🎒', ':scooter:': '🛴', ':scorpion:': '🦂', ':scream:': '😱', ':scream_cat:': '🙀', ':scroll:': '📜', ':seat:': '💺', ':second_place_medal:': '🥈', ':second_place:': '🥈', ':see_no_evil:': '🙈', ':seedling:': '🌱', ':selfie:': '🤳', ':paella:': '🥘', ':shallow_pan_of_food:': '🥘', ':shark:': '🦈', ':shaved_ice:': 'ðŸ§', ':sheep:': 'ðŸ‘', ':shell:': 'ðŸš', ':shield:': '🛡ï¸', ':ship:': '🚢', ':shirt:': '👕', ':shopping_bags:': 'ðŸ›ï¸', ':shopping_trolley:': '🛒', ':shopping_cart:': '🛒', ':shorts:': '🩳', ':shower:': '🚿', ':shrimp:': 'ðŸ¦', ':shushing_face:': '🤫', ':signal_strength:': '📶', ':six_pointed_star:': '🔯', ':skateboard:': '🛹', ':ski:': '🎿', ':skeleton:': '💀', ':skull:': '💀', ':skunk:': '🦨', ':sled:': '🛷', ':sleeping:': '😴', ':sleeping_accommodation:': '🛌', ':sleepy:': '😪', ':slightly_frowning_face:': 'ðŸ™', ':slight_frown:': 'ðŸ™', ':slightly_smiling_face:': '🙂', ':slight_smile:': '🙂', ':slot_machine:': '🎰', ':sloth:': '🦥', ':small_blue_diamond:': '🔹', ':small_orange_diamond:': '🔸', ':small_red_triangle:': '🔺', ':small_red_triangle_down:': '🔻', ':smile:': '😄', ':smile_cat:': '😸', ':smiley:': '😃', ':smiley_cat:': '😺', ':smiling_face_with_3_hearts:': '🥰', ':smiling_imp:': '😈', ':smirk:': 'ðŸ˜', ':smirk_cat:': '😼', ':smoking:': '🚬', ':snail:': 'ðŸŒ', ':snake:': 'ðŸ', ':sneeze:': '🤧', ':sneezing_face:': '🤧', ':snowboarder:': 'ðŸ‚', ':soap:': '🧼', ':sob:': 'ðŸ˜', ':socks:': '🧦', ':softball:': '🥎', ':soon:': '🔜', ':sos:': '🆘', ':sound:': '🔉', ':space_invader:': '👾', ':spaghetti:': 'ðŸ', ':sparkler:': '🎇', ':sparkling_heart:': '💖', ':speak_no_evil:': '🙊', ':speaker:': '🔈', ':speaking_head_in_silhouette:': '🗣ï¸', ':speaking_head:': '🗣ï¸', ':speech_balloon:': '💬', ':left_speech_bubble:': '🗨ï¸', ':speech_left:': '🗨ï¸', ':speedboat:': '🚤', ':spider:': '🕷ï¸', ':spider_web:': '🕸ï¸', ':sponge:': '🧽', ':spoon:': '🥄', ':squeeze_bottle:': '🧴', ':squid:': '🦑', ':stadium:': 'ðŸŸï¸', ':star2:': '🌟', ':star_struck:': '🤩', ':stars:': '🌠', ':station:': '🚉', ':statue_of_liberty:': '🗽', ':steam_locomotive:': '🚂', ':stethoscope:': '🩺', ':stew:': 'ðŸ²', ':straight_ruler:': 'ðŸ“', ':strawberry:': 'ðŸ“', ':stuck_out_tongue:': '😛', ':stuck_out_tongue_closed_eyes:': 'ðŸ˜', ':stuck_out_tongue_winking_eye:': '😜', ':stuffed_pita:': '🥙', ':stuffed_flatbread:': '🥙', ':sun_with_face:': '🌞', ':sunflower:': '🌻', ':sunglasses:': '😎', ':sunrise:': '🌅', ':sunrise_over_mountains:': '🌄', ':superhero:': '🦸', ':supervillain:': '🦹', ':sushi:': 'ðŸ£', ':suspension_railway:': '🚟', ':swan:': '🦢', ':sweat:': '😓', ':sweat_drops:': '💦', ':sweat_smile:': '😅', ':sweet_potato:': 'ðŸ ', ':symbols:': '🔣', ':synagogue:': 'ðŸ•', ':syringe:': '💉', ':t_rex:': '🦖', ':taco:': '🌮', ':tada:': '🎉', ':takeout_box:': '🥡', ':tanabata_tree:': '🎋', ':tangerine:': 'ðŸŠ', ':taxi:': '🚕', ':tea:': 'ðŸµ', ':teddy_bear:': '🧸', ':telephone_receiver:': '📞', ':telescope:': 'ðŸ”', ':tennis:': '🎾', ':test_tube:': '🧪', ':thermometer:': '🌡ï¸', ':face_with_thermometer:': '🤒', ':thermometer_face:': '🤒', ':thinking_face:': '🤔', ':thinking:': '🤔', ':third_place_medal:': '🥉', ':third_place:': '🥉', ':thought_balloon:': 'ðŸ’', ':thread:': '🧵', ':-1:': '👎', ':thumbdown:': '👎', ':thumbsdown:': '👎', ':+1:': 'ðŸ‘', ':thumbup:': 'ðŸ‘', ':thumbsup:': 'ðŸ‘', ':ticket:': '🎫', ':admission_tickets:': '🎟ï¸', ':tickets:': '🎟ï¸', ':tiger2:': 'ðŸ…', ':tiger:': 'ðŸ¯', ':tired_face:': '😫', ':toilet:': '🚽', ':tokyo_tower:': '🗼', ':tomato:': 'ðŸ…', ':tone1:': 'ðŸ»', ':tone2:': 'ðŸ¼', ':tone3:': 'ðŸ½', ':tone4:': 'ðŸ¾', ':tone5:': 'ðŸ¿', ':tongue:': '👅', ':toolbox:': '🧰', ':hammer_and_wrench:': '🛠ï¸', ':tools:': '🛠ï¸', ':tooth:': '🦷', ':top:': 'ðŸ”', ':tophat:': '🎩', ':trackball:': '🖲ï¸', ':tractor:': '🚜', ':traffic_light:': '🚥', ':train2:': '🚆', ':train:': '🚋', ':tram:': '🚊', ':triangular_flag_on_post:': '🚩', ':triangular_ruler:': 'ðŸ“', ':trident:': '🔱', ':triumph:': '😤', ':trolleybus:': '🚎', ':trophy:': 'ðŸ†', ':tropical_drink:': 'ðŸ¹', ':tropical_fish:': 'ðŸ ', ':truck:': '🚚', ':trumpet:': '🎺', ':tulip:': '🌷', ':whisky:': '🥃', ':tumbler_glass:': '🥃', ':turkey:': '🦃', ':turtle:': 'ðŸ¢', ':tv:': '📺', ':twisted_rightwards_arrows:': '🔀', ':two_hearts:': '💕', ':two_men_holding_hands:': '👬', ':two_women_holding_hands:': 'ðŸ‘', ':u5272:': '🈹', ':u5408:': '🈴', ':u55b6:': '🈺', ':u6307:': '🈯', ':u6708:': '🈷ï¸', ':u6709:': '🈶', ':u6e80:': '🈵', ':u7121:': '🈚', ':u7533:': '🈸', ':u7981:': '🈲', ':u7a7a:': '🈳', ':unamused:': '😒', ':underage:': '🔞', ':unicorn_face:': '🦄', ':unicorn:': '🦄', ':unlock:': '🔓', ':up:': '🆙', ':upside_down_face:': '🙃', ':upside_down:': '🙃', ':vampire:': '🧛', ':vertical_traffic_light:': '🚦', ':vhs:': '📼', ':vibration_mode:': '📳', ':video_camera:': '📹', ':video_game:': '🎮', ':violin:': '🎻', ':volcano:': '🌋', ':volleyball:': 'ðŸ', ':vs:': '🆚', ':raised_hand_with_part_between_middle_and_ring_fingers:': '🖖', ':vulcan:': '🖖', ':waffle:': '🧇', ':waning_crescent_moon:': '🌘', ':waning_gibbous_moon:': '🌖', ':wastebasket:': '🗑ï¸', ':water_buffalo:': 'ðŸƒ', ':watermelon:': 'ðŸ‰', ':wave:': '👋', ':waxing_crescent_moon:': '🌒', ':waxing_gibbous_moon:': '🌔', ':wc:': '🚾', ':weary:': '😩', ':wedding:': '💒', ':whale2:': 'ðŸ‹', ':whale:': 'ðŸ³', ':white_flower:': '💮', ':white_haired:': '🦳', ':white_heart:': 'ðŸ¤', ':white_square_button:': '🔳', ':white_sun_behind_cloud:': '🌥ï¸', ':white_sun_cloud:': '🌥ï¸', ':white_sun_behind_cloud_with_rain:': '🌦ï¸', ':white_sun_rain_cloud:': '🌦ï¸', ':white_sun_with_small_cloud:': '🌤ï¸', ':white_sun_small_cloud:': '🌤ï¸', ':wilted_flower:': '🥀', ':wilted_rose:': '🥀', ':wind_blowing_face:': '🌬ï¸', ':wind_chime:': 'ðŸŽ', ':wine_glass:': 'ðŸ·', ':wink:': '😉', ':wolf:': 'ðŸº', ':woman:': '👩', ':woman_with_headscarf:': '🧕', ':womans_clothes:': '👚', ':womans_flat_shoe:': '🥿', ':womans_hat:': '👒', ':womens:': '🚺', ':woozy_face:': '🥴', ':worried:': '😟', ':wrench:': '🔧', ':yarn:': '🧶', ':yawning_face:': '🥱', ':yellow_circle:': '🟡', ':yellow_heart:': '💛', ':yellow_square:': '🟨', ':yen:': '💴', ':yo_yo:': '🪀', ':yum:': '😋', ':zany_face:': '🤪', ':zebra:': '🦓', ':zipper_mouth_face:': 'ðŸ¤', ':zipper_mouth:': 'ðŸ¤', ':zombie:': '🧟', ':zzz:': '💤', ':airplane:': '✈ï¸', ':alarm_clock:': 'â°', ':alembic:': 'âš—ï¸', ':anchor:': 'âš“', ':aquarius:': 'â™’', ':aries:': '♈', ':arrow_backward:': 'â—€ï¸', ':arrow_double_down:': 'â¬', ':arrow_double_up:': 'â«', ':arrow_down:': '⬇ï¸', ':arrow_forward:': 'â–¶ï¸', ':arrow_heading_down:': '⤵ï¸', ':arrow_heading_up:': '⤴ï¸', ':arrow_left:': '⬅ï¸', ':arrow_lower_left:': '↙ï¸', ':arrow_lower_right:': '↘ï¸', ':arrow_right:': 'âž¡ï¸', ':arrow_right_hook:': '↪ï¸', ':arrow_up:': '⬆ï¸', ':arrow_up_down:': '↕ï¸', ':arrow_upper_left:': '↖ï¸', ':arrow_upper_right:': '↗ï¸', ':asterisk_symbol:': '*ï¸', ':atom_symbol:': 'âš›ï¸', ':atom:': 'âš›ï¸', ':ballot_box_with_check:': '☑ï¸', ':bangbang:': '‼ï¸', ':baseball:': 'âš¾', ':umbrella_on_ground:': 'â›±ï¸', ':beach_umbrella:': 'â›±ï¸', ':biohazard_sign:': '☣ï¸', ':biohazard:': '☣ï¸', ':black_circle:': 'âš«', ':black_large_square:': '⬛', ':black_medium_small_square:': 'â—¾', ':black_medium_square:': 'â—¼ï¸', ':black_nib:': '✒ï¸', ':black_small_square:': 'â–ªï¸', ':cancer:': '♋', ':capricorn:': '♑', ':chains:': '⛓ï¸', ':chess_pawn:': '♟ï¸', ':church:': '⛪', ':cloud:': 'â˜ï¸', ':clubs:': '♣ï¸', ':coffee:': '☕', ':coffin:': 'âš°ï¸', ':comet:': '☄ï¸', ':congratulations:': '㊗ï¸', ':copyright:': '©ï¸', ':latin_cross:': 'âœï¸', ':cross:': 'âœï¸', ':crossed_swords:': 'âš”ï¸', ':curly_loop:': 'âž°', ':diamonds:': '♦ï¸', ':digit_eight:': '8ï¸', ':digit_five:': '5ï¸', ':digit_four:': '4ï¸', ':digit_nine:': '9ï¸', ':digit_one:': '1ï¸', ':digit_seven:': '7ï¸', ':digit_six:': '6ï¸', ':digit_three:': '3ï¸', ':digit_two:': '2ï¸', ':digit_zero:': '0ï¸', ':eight_pointed_black_star:': '✴ï¸', ':eight_spoked_asterisk:': '✳ï¸', ':eject_symbol:': 'âï¸', ':eject:': 'âï¸', ':envelope:': '✉ï¸', ':exclamation:': 'â—', ':fast_forward:': 'â©', ':female_sign:': '♀ï¸', ':ferry:': 'â›´ï¸', ':fist:': '✊', ':fleur-de-lis:': 'âšœï¸', ':fountain:': '⛲', ':white_frowning_face:': '☹ï¸', ':frowning2:': '☹ï¸', ':fuelpump:': '⛽', ':gear:': 'âš™ï¸', ':gemini:': '♊', ':golf:': '⛳', ':grey_exclamation:': 'â•', ':grey_question:': 'â”', ':hammer_and_pick:': 'âš’ï¸', ':hammer_pick:': 'âš’ï¸', ':heart:': 'â¤ï¸', ':heavy_heart_exclamation_mark_ornament:': 'â£ï¸', ':heart_exclamation:': 'â£ï¸', ':hearts:': '♥ï¸', ':heavy_check_mark:': '✔ï¸', ':heavy_division_sign:': 'âž—', ':heavy_minus_sign:': 'âž–', ':heavy_multiplication_x:': '✖ï¸', ':heavy_plus_sign:': 'âž•', ':helmet_with_white_cross:': '⛑ï¸', ':helmet_with_cross:': '⛑ï¸', ':hotsprings:': '♨ï¸', ':hourglass:': '⌛', ':hourglass_flowing_sand:': 'â³', ':ice_skate:': '⛸ï¸', ':infinity:': '♾ï¸', ':information_source:': 'ℹï¸', ':interrobang:': 'â‰ï¸', ':keyboard:': '⌨ï¸', ':left_right_arrow:': '↔ï¸', ':leftwards_arrow_with_hook:': '↩ï¸', ':leo:': '♌', ':libra:': '♎', ':loop:': 'âž¿', ':m:': 'â“‚ï¸', ':male_sign:': '♂ï¸', ':medical_symbol:': 'âš•ï¸', ':mountain:': 'â›°ï¸', ':negative_squared_cross_mark:': 'âŽ', ':no_entry:': 'â›”', ':o:': 'â•', ':ophiuchus:': '⛎', ':orthodox_cross:': '☦ï¸', ':part_alternation_mark:': '〽ï¸', ':partly_sunny:': 'â›…', ':double_vertical_bar:': 'â¸ï¸', ':pause_button:': 'â¸ï¸', ':peace_symbol:': '☮ï¸', ':peace:': '☮ï¸', ':pencil2:': 'âœï¸', ':basketball_player:': '⛹ï¸', ':person_with_ball:': '⛹ï¸', ':person_bouncing_ball:': '⛹ï¸', ':pick:': 'â›ï¸', ':pisces:': '♓', ':play_pause:': 'â¯ï¸', ':point_up:': 'â˜ï¸', ':pound_symbol:': '#ï¸', ':question:': 'â“', ':radioactive_sign:': '☢ï¸', ':radioactive:': '☢ï¸', ':raised_hand:': '✋', ':record_button:': 'âºï¸', ':recycle:': 'â™»ï¸', ':registered:': '®ï¸', ':relaxed:': '☺ï¸', ':rewind:': 'âª', ':sagittarius:': 'â™', ':sailboat:': '⛵', ':scales:': 'âš–ï¸', ':scissors:': '✂ï¸', ':scorpius:': 'â™', ':secret:': '㊙ï¸', ':shamrock:': '☘ï¸', ':shinto_shrine:': '⛩ï¸', ':skier:': 'â›·ï¸', ':skull_and_crossbones:': '☠ï¸', ':skull_crossbones:': '☠ï¸', ':snowflake:': 'â„ï¸', ':snowman2:': '☃ï¸', ':snowman:': '⛄', ':soccer:': 'âš½', ':spades:': 'â™ ï¸', ':sparkle:': 'â‡ï¸', ':sparkles:': '✨', ':star:': 'â', ':star_and_crescent:': '☪ï¸', ':star_of_david:': '✡ï¸', ':stop_button:': 'â¹ï¸', ':stopwatch:': 'â±ï¸', ':sunny:': '☀ï¸', ':taurus:': '♉', ':telephone:': '☎ï¸', ':tent:': '⛺', ':thunder_cloud_and_rain:': '⛈ï¸', ':thunder_cloud_rain:': '⛈ï¸', ':timer_clock:': 'â²ï¸', ':timer:': 'â²ï¸', ':tm:': 'â„¢ï¸', ':next_track:': 'âï¸', ':track_next:': 'âï¸', ':previous_track:': 'â®ï¸', ':track_previous:': 'â®ï¸', ':umbrella2:': '☂ï¸', ':umbrella:': '☔', ':funeral_urn:': 'âš±ï¸', ':urn:': 'âš±ï¸', ':v:': '✌ï¸', ':virgo:': 'â™', ':warning:': 'âš ï¸', ':watch:': '⌚', ':wavy_dash:': '〰ï¸', ':wheel_of_dharma:': '☸ï¸', ':wheelchair:': '♿', ':white_check_mark:': '✅', ':white_circle:': '⚪', ':white_large_square:': '⬜', ':white_medium_small_square:': 'â—½', ':white_medium_square:': 'â—»ï¸', ':white_small_square:': 'â–«ï¸', ':writing_hand:': 'âœï¸', ':x:': 'âŒ', ':yin_yang:': '☯ï¸', ':zap:': 'âš¡' }; +const emojis = { + ':england:': 'ðŸ´ó §ó ¢ó ¥ó ®ó §ó ¿', + ':scotland:': 'ðŸ´ó §ó ¢ó ³ó £ó ´ó ¿', + ':wales:': 'ðŸ´ó §ó ¢ó ·ó ¬ó ³ó ¿', + ':men_holding_hands_medium_light_skin_tone_light_skin_tone:': '👨ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_tone2_tone1:': '👨ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_medium_skin_tone_light_skin_tone:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_tone3_tone1:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_tone3_tone2:': '👨ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_tone4_tone1:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_tone4_tone2:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':men_holding_hands_tone4_tone3:': '👨ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':men_holding_hands_dark_skin_tone_light_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_tone5_tone1:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':men_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_tone5_tone2:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':men_holding_hands_dark_skin_tone_medium_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':men_holding_hands_tone5_tone3:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':men_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':men_holding_hands_tone5_tone4:': '👨ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':people_holding_hands_light_skin_tone:': '🧑ðŸ»â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_tone1:': '🧑ðŸ»â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_medium_light_skin_tone:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_tone2:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_medium_light_skin_tone_light_skin_tone:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_tone2_tone1:': '🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_medium_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_tone3:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_medium_skin_tone_light_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_tone3_tone1:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_medium_skin_tone_medium_light_skin_tone:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_tone3_tone2:': '🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_medium_dark_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¾', + ':people_holding_hands_tone4:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¾', + ':people_holding_hands_medium_dark_skin_tone_light_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_tone4_tone1:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_tone4_tone2:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_tone4_tone3:': '🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_dark_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¿', + ':people_holding_hands_tone5:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¿', + ':people_holding_hands_dark_skin_tone_light_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_tone5_tone1:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ»', + ':people_holding_hands_dark_skin_tone_medium_light_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_tone5_tone2:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¼', + ':people_holding_hands_dark_skin_tone_medium_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_tone5_tone3:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ½', + ':people_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¾', + ':people_holding_hands_tone5_tone4:': '🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¾', + ':woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_tone1_tone2:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_light_skin_tone_medium_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_tone1_tone3:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_tone1_tone4:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_light_skin_tone_dark_skin_tone:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_tone1_tone5:': '👩ðŸ»â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_tone2_tone1:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_tone2_tone3:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_tone2_tone4:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_tone2_tone5:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_medium_skin_tone_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_tone3_tone1:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_tone3_tone2:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_tone3_tone4:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_tone3_tone5:': '👩ðŸ½â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_tone4_tone1:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_tone4_tone2:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_tone4_tone3:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_tone4_tone5:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘¨ðŸ¿', + ':woman_and_man_holding_hands_dark_skin_tone_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_tone5_tone1:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ»', + ':woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_tone5_tone2:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¼', + ':woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_tone5_tone3:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ½', + ':woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':woman_and_man_holding_hands_tone5_tone4:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘¨ðŸ¾', + ':women_holding_hands_medium_light_skin_tone_light_skin_tone:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_tone2_tone1:': '👩ðŸ¼â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_medium_skin_tone_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_tone3_tone1:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_medium_skin_tone_medium_light_skin_tone:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_tone3_tone2:': '👩ðŸ½â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_medium_dark_skin_tone_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_tone4_tone1:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_tone4_tone2:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_medium_dark_skin_tone_medium_skin_tone:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ½', + ':women_holding_hands_tone4_tone3:': '👩ðŸ¾â€ðŸ¤â€ðŸ‘©ðŸ½', + ':women_holding_hands_dark_skin_tone_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_tone5_tone1:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ»', + ':women_holding_hands_dark_skin_tone_medium_light_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_tone5_tone2:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¼', + ':women_holding_hands_dark_skin_tone_medium_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ½', + ':women_holding_hands_tone5_tone3:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ½', + ':women_holding_hands_dark_skin_tone_medium_dark_skin_tone:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¾', + ':women_holding_hands_tone5_tone4:': '👩ðŸ¿â€ðŸ¤â€ðŸ‘©ðŸ¾', + ':family_mmbb:': '👨â€ðŸ‘¨â€ðŸ‘¦â€ðŸ‘¦', + ':family_mmgb:': '👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘¦', + ':family_mmgg:': '👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘§', + ':family_mwbb:': '👨â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦', + ':family_mwgb:': '👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦', + ':family_mwgg:': '👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§', + ':family_wwbb:': '👩â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦', + ':family_wwgb:': '👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦', + ':family_wwgg:': '👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§', + ':couplekiss_mm:': '👨â€â¤ï¸â€ðŸ’‹ðŸ‘¨', + ':kiss_mm:': '👨â€â¤ï¸â€ðŸ’‹ðŸ‘¨', + ':kiss_woman_man:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘¨', + ':couplekiss_ww:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘©', + ':kiss_ww:': '👩â€â¤ï¸â€ðŸ’‹ðŸ‘©', + ':family_man_boy_boy:': '👨â€ðŸ‘¦â€ðŸ‘¦', + ':family_man_girl_boy:': '👨â€ðŸ‘§â€ðŸ‘¦', + ':family_man_girl_girl:': '👨â€ðŸ‘§â€ðŸ‘§', + ':family_man_woman_boy:': '👨â€ðŸ‘©â€ðŸ‘¦', + ':family_mmb:': '👨â€ðŸ‘¨â€ðŸ‘¦', + ':family_mmg:': '👨â€ðŸ‘¨â€ðŸ‘§', + ':family_mwg:': '👨â€ðŸ‘©â€ðŸ‘§', + ':family_woman_boy_boy:': '👩â€ðŸ‘¦â€ðŸ‘¦', + ':family_woman_girl_boy:': '👩â€ðŸ‘§â€ðŸ‘¦', + ':family_woman_girl_girl:': '👩â€ðŸ‘§â€ðŸ‘§', + ':family_wwb:': '👩â€ðŸ‘©â€ðŸ‘¦', + ':family_wwg:': '👩â€ðŸ‘©â€ðŸ‘§', + ':man_artist_light_skin_tone:': '👨ðŸ»â€ðŸŽ¨', + ':man_artist_tone1:': '👨ðŸ»â€ðŸŽ¨', + ':man_artist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ¨', + ':man_artist_tone2:': '👨ðŸ¼â€ðŸŽ¨', + ':man_artist_medium_skin_tone:': '👨ðŸ½â€ðŸŽ¨', + ':man_artist_tone3:': '👨ðŸ½â€ðŸŽ¨', + ':man_artist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ¨', + ':man_artist_tone4:': '👨ðŸ¾â€ðŸŽ¨', + ':man_artist_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ¨', + ':man_artist_tone5:': '👨ðŸ¿â€ðŸŽ¨', + ':man_astronaut_light_skin_tone:': '👨ðŸ»â€ðŸš€', + ':man_astronaut_tone1:': '👨ðŸ»â€ðŸš€', + ':man_astronaut_medium_light_skin_tone:': '👨ðŸ¼â€ðŸš€', + ':man_astronaut_tone2:': '👨ðŸ¼â€ðŸš€', + ':man_astronaut_medium_skin_tone:': '👨ðŸ½â€ðŸš€', + ':man_astronaut_tone3:': '👨ðŸ½â€ðŸš€', + ':man_astronaut_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸš€', + ':man_astronaut_tone4:': '👨ðŸ¾â€ðŸš€', + ':man_astronaut_dark_skin_tone:': '👨ðŸ¿â€ðŸš€', + ':man_astronaut_tone5:': '👨ðŸ¿â€ðŸš€', + ':man_bald_light_skin_tone:': '👨ðŸ»â€ðŸ¦²', + ':man_bald_tone1:': '👨ðŸ»â€ðŸ¦²', + ':man_bald_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦²', + ':man_bald_tone2:': '👨ðŸ¼â€ðŸ¦²', + ':man_bald_medium_skin_tone:': '👨ðŸ½â€ðŸ¦²', + ':man_bald_tone3:': '👨ðŸ½â€ðŸ¦²', + ':man_bald_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦²', + ':man_bald_tone4:': '👨ðŸ¾â€ðŸ¦²', + ':man_bald_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦²', + ':man_bald_tone5:': '👨ðŸ¿â€ðŸ¦²', + ':man_cook_light_skin_tone:': '👨ðŸ»â€ðŸ³', + ':man_cook_tone1:': '👨ðŸ»â€ðŸ³', + ':man_cook_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ³', + ':man_cook_tone2:': '👨ðŸ¼â€ðŸ³', + ':man_cook_medium_skin_tone:': '👨ðŸ½â€ðŸ³', + ':man_cook_tone3:': '👨ðŸ½â€ðŸ³', + ':man_cook_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ³', + ':man_cook_tone4:': '👨ðŸ¾â€ðŸ³', + ':man_cook_dark_skin_tone:': '👨ðŸ¿â€ðŸ³', + ':man_cook_tone5:': '👨ðŸ¿â€ðŸ³', + ':man_curly_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦±', + ':man_curly_haired_tone1:': '👨ðŸ»â€ðŸ¦±', + ':man_curly_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦±', + ':man_curly_haired_tone2:': '👨ðŸ¼â€ðŸ¦±', + ':man_curly_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦±', + ':man_curly_haired_tone3:': '👨ðŸ½â€ðŸ¦±', + ':man_curly_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦±', + ':man_curly_haired_tone4:': '👨ðŸ¾â€ðŸ¦±', + ':man_curly_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦±', + ':man_curly_haired_tone5:': '👨ðŸ¿â€ðŸ¦±', + ':man_factory_worker_light_skin_tone:': '👨ðŸ»â€ðŸ', + ':man_factory_worker_tone1:': '👨ðŸ»â€ðŸ', + ':man_factory_worker_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ', + ':man_factory_worker_tone2:': '👨ðŸ¼â€ðŸ', + ':man_factory_worker_medium_skin_tone:': '👨ðŸ½â€ðŸ', + ':man_factory_worker_tone3:': '👨ðŸ½â€ðŸ', + ':man_factory_worker_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ', + ':man_factory_worker_tone4:': '👨ðŸ¾â€ðŸ', + ':man_factory_worker_dark_skin_tone:': '👨ðŸ¿â€ðŸ', + ':man_factory_worker_tone5:': '👨ðŸ¿â€ðŸ', + ':man_farmer_light_skin_tone:': '👨ðŸ»â€ðŸŒ¾', + ':man_farmer_tone1:': '👨ðŸ»â€ðŸŒ¾', + ':man_farmer_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŒ¾', + ':man_farmer_tone2:': '👨ðŸ¼â€ðŸŒ¾', + ':man_farmer_medium_skin_tone:': '👨ðŸ½â€ðŸŒ¾', + ':man_farmer_tone3:': '👨ðŸ½â€ðŸŒ¾', + ':man_farmer_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŒ¾', + ':man_farmer_tone4:': '👨ðŸ¾â€ðŸŒ¾', + ':man_farmer_dark_skin_tone:': '👨ðŸ¿â€ðŸŒ¾', + ':man_farmer_tone5:': '👨ðŸ¿â€ðŸŒ¾', + ':man_firefighter_light_skin_tone:': '👨ðŸ»â€ðŸš’', + ':man_firefighter_tone1:': '👨ðŸ»â€ðŸš’', + ':man_firefighter_medium_light_skin_tone:': '👨ðŸ¼â€ðŸš’', + ':man_firefighter_tone2:': '👨ðŸ¼â€ðŸš’', + ':man_firefighter_medium_skin_tone:': '👨ðŸ½â€ðŸš’', + ':man_firefighter_tone3:': '👨ðŸ½â€ðŸš’', + ':man_firefighter_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸš’', + ':man_firefighter_tone4:': '👨ðŸ¾â€ðŸš’', + ':man_firefighter_dark_skin_tone:': '👨ðŸ¿â€ðŸš’', + ':man_firefighter_tone5:': '👨ðŸ¿â€ðŸš’', + ':man_in_manual_wheelchair_light_skin_tone:': '👨ðŸ»â€ðŸ¦½', + ':man_in_manual_wheelchair_tone1:': '👨ðŸ»â€ðŸ¦½', + ':man_in_manual_wheelchair_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦½', + ':man_in_manual_wheelchair_tone2:': '👨ðŸ¼â€ðŸ¦½', + ':man_in_manual_wheelchair_medium_skin_tone:': '👨ðŸ½â€ðŸ¦½', + ':man_in_manual_wheelchair_tone3:': '👨ðŸ½â€ðŸ¦½', + ':man_in_manual_wheelchair_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦½', + ':man_in_manual_wheelchair_tone4:': '👨ðŸ¾â€ðŸ¦½', + ':man_in_manual_wheelchair_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦½', + ':man_in_manual_wheelchair_tone5:': '👨ðŸ¿â€ðŸ¦½', + ':man_in_motorized_wheelchair_light_skin_tone:': '👨ðŸ»â€ðŸ¦¼', + ':man_in_motorized_wheelchair_tone1:': '👨ðŸ»â€ðŸ¦¼', + ':man_in_motorized_wheelchair_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦¼', + ':man_in_motorized_wheelchair_tone2:': '👨ðŸ¼â€ðŸ¦¼', + ':man_in_motorized_wheelchair_medium_skin_tone:': '👨ðŸ½â€ðŸ¦¼', + ':man_in_motorized_wheelchair_tone3:': '👨ðŸ½â€ðŸ¦¼', + ':man_in_motorized_wheelchair_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦¼', + ':man_in_motorized_wheelchair_tone4:': '👨ðŸ¾â€ðŸ¦¼', + ':man_in_motorized_wheelchair_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦¼', + ':man_in_motorized_wheelchair_tone5:': '👨ðŸ¿â€ðŸ¦¼', + ':man_mechanic_light_skin_tone:': '👨ðŸ»â€ðŸ”§', + ':man_mechanic_tone1:': '👨ðŸ»â€ðŸ”§', + ':man_mechanic_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ”§', + ':man_mechanic_tone2:': '👨ðŸ¼â€ðŸ”§', + ':man_mechanic_medium_skin_tone:': '👨ðŸ½â€ðŸ”§', + ':man_mechanic_tone3:': '👨ðŸ½â€ðŸ”§', + ':man_mechanic_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ”§', + ':man_mechanic_tone4:': '👨ðŸ¾â€ðŸ”§', + ':man_mechanic_dark_skin_tone:': '👨ðŸ¿â€ðŸ”§', + ':man_mechanic_tone5:': '👨ðŸ¿â€ðŸ”§', + ':man_office_worker_light_skin_tone:': '👨ðŸ»â€ðŸ’¼', + ':man_office_worker_tone1:': '👨ðŸ»â€ðŸ’¼', + ':man_office_worker_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ’¼', + ':man_office_worker_tone2:': '👨ðŸ¼â€ðŸ’¼', + ':man_office_worker_medium_skin_tone:': '👨ðŸ½â€ðŸ’¼', + ':man_office_worker_tone3:': '👨ðŸ½â€ðŸ’¼', + ':man_office_worker_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ’¼', + ':man_office_worker_tone4:': '👨ðŸ¾â€ðŸ’¼', + ':man_office_worker_dark_skin_tone:': '👨ðŸ¿â€ðŸ’¼', + ':man_office_worker_tone5:': '👨ðŸ¿â€ðŸ’¼', + ':man_red_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦°', + ':man_red_haired_tone1:': '👨ðŸ»â€ðŸ¦°', + ':man_red_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦°', + ':man_red_haired_tone2:': '👨ðŸ¼â€ðŸ¦°', + ':man_red_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦°', + ':man_red_haired_tone3:': '👨ðŸ½â€ðŸ¦°', + ':man_red_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦°', + ':man_red_haired_tone4:': '👨ðŸ¾â€ðŸ¦°', + ':man_red_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦°', + ':man_red_haired_tone5:': '👨ðŸ¿â€ðŸ¦°', + ':man_scientist_light_skin_tone:': '👨ðŸ»â€ðŸ”¬', + ':man_scientist_tone1:': '👨ðŸ»â€ðŸ”¬', + ':man_scientist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ”¬', + ':man_scientist_tone2:': '👨ðŸ¼â€ðŸ”¬', + ':man_scientist_medium_skin_tone:': '👨ðŸ½â€ðŸ”¬', + ':man_scientist_tone3:': '👨ðŸ½â€ðŸ”¬', + ':man_scientist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ”¬', + ':man_scientist_tone4:': '👨ðŸ¾â€ðŸ”¬', + ':man_scientist_dark_skin_tone:': '👨ðŸ¿â€ðŸ”¬', + ':man_scientist_tone5:': '👨ðŸ¿â€ðŸ”¬', + ':man_singer_light_skin_tone:': '👨ðŸ»â€ðŸŽ¤', + ':man_singer_tone1:': '👨ðŸ»â€ðŸŽ¤', + ':man_singer_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ¤', + ':man_singer_tone2:': '👨ðŸ¼â€ðŸŽ¤', + ':man_singer_medium_skin_tone:': '👨ðŸ½â€ðŸŽ¤', + ':man_singer_tone3:': '👨ðŸ½â€ðŸŽ¤', + ':man_singer_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ¤', + ':man_singer_tone4:': '👨ðŸ¾â€ðŸŽ¤', + ':man_singer_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ¤', + ':man_singer_tone5:': '👨ðŸ¿â€ðŸŽ¤', + ':man_student_light_skin_tone:': '👨ðŸ»â€ðŸŽ“', + ':man_student_tone1:': '👨ðŸ»â€ðŸŽ“', + ':man_student_medium_light_skin_tone:': '👨ðŸ¼â€ðŸŽ“', + ':man_student_tone2:': '👨ðŸ¼â€ðŸŽ“', + ':man_student_medium_skin_tone:': '👨ðŸ½â€ðŸŽ“', + ':man_student_tone3:': '👨ðŸ½â€ðŸŽ“', + ':man_student_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸŽ“', + ':man_student_tone4:': '👨ðŸ¾â€ðŸŽ“', + ':man_student_dark_skin_tone:': '👨ðŸ¿â€ðŸŽ“', + ':man_student_tone5:': '👨ðŸ¿â€ðŸŽ“', + ':man_teacher_light_skin_tone:': '👨ðŸ»â€ðŸ«', + ':man_teacher_tone1:': '👨ðŸ»â€ðŸ«', + ':man_teacher_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ«', + ':man_teacher_tone2:': '👨ðŸ¼â€ðŸ«', + ':man_teacher_medium_skin_tone:': '👨ðŸ½â€ðŸ«', + ':man_teacher_tone3:': '👨ðŸ½â€ðŸ«', + ':man_teacher_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ«', + ':man_teacher_tone4:': '👨ðŸ¾â€ðŸ«', + ':man_teacher_dark_skin_tone:': '👨ðŸ¿â€ðŸ«', + ':man_teacher_tone5:': '👨ðŸ¿â€ðŸ«', + ':man_technologist_light_skin_tone:': '👨ðŸ»â€ðŸ’»', + ':man_technologist_tone1:': '👨ðŸ»â€ðŸ’»', + ':man_technologist_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ’»', + ':man_technologist_tone2:': '👨ðŸ¼â€ðŸ’»', + ':man_technologist_medium_skin_tone:': '👨ðŸ½â€ðŸ’»', + ':man_technologist_tone3:': '👨ðŸ½â€ðŸ’»', + ':man_technologist_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ’»', + ':man_technologist_tone4:': '👨ðŸ¾â€ðŸ’»', + ':man_technologist_dark_skin_tone:': '👨ðŸ¿â€ðŸ’»', + ':man_technologist_tone5:': '👨ðŸ¿â€ðŸ’»', + ':man_white_haired_light_skin_tone:': '👨ðŸ»â€ðŸ¦³', + ':man_white_haired_tone1:': '👨ðŸ»â€ðŸ¦³', + ':man_white_haired_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦³', + ':man_white_haired_tone2:': '👨ðŸ¼â€ðŸ¦³', + ':man_white_haired_medium_skin_tone:': '👨ðŸ½â€ðŸ¦³', + ':man_white_haired_tone3:': '👨ðŸ½â€ðŸ¦³', + ':man_white_haired_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦³', + ':man_white_haired_tone4:': '👨ðŸ¾â€ðŸ¦³', + ':man_white_haired_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦³', + ':man_white_haired_tone5:': '👨ðŸ¿â€ðŸ¦³', + ':man_with_probing_cane_light_skin_tone:': '👨ðŸ»â€ðŸ¦¯', + ':man_with_probing_cane_tone1:': '👨ðŸ»â€ðŸ¦¯', + ':man_with_probing_cane_medium_light_skin_tone:': '👨ðŸ¼â€ðŸ¦¯', + ':man_with_probing_cane_tone2:': '👨ðŸ¼â€ðŸ¦¯', + ':man_with_probing_cane_medium_skin_tone:': '👨ðŸ½â€ðŸ¦¯', + ':man_with_probing_cane_tone3:': '👨ðŸ½â€ðŸ¦¯', + ':man_with_probing_cane_medium_dark_skin_tone:': '👨ðŸ¾â€ðŸ¦¯', + ':man_with_probing_cane_tone4:': '👨ðŸ¾â€ðŸ¦¯', + ':man_with_probing_cane_dark_skin_tone:': '👨ðŸ¿â€ðŸ¦¯', + ':man_with_probing_cane_tone5:': '👨ðŸ¿â€ðŸ¦¯', + ':people_holding_hands:': '🧑â€ðŸ¤â€ðŸ§‘', + ':woman_artist_light_skin_tone:': '👩ðŸ»â€ðŸŽ¨', + ':woman_artist_tone1:': '👩ðŸ»â€ðŸŽ¨', + ':woman_artist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ¨', + ':woman_artist_tone2:': '👩ðŸ¼â€ðŸŽ¨', + ':woman_artist_medium_skin_tone:': '👩ðŸ½â€ðŸŽ¨', + ':woman_artist_tone3:': '👩ðŸ½â€ðŸŽ¨', + ':woman_artist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ¨', + ':woman_artist_tone4:': '👩ðŸ¾â€ðŸŽ¨', + ':woman_artist_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ¨', + ':woman_artist_tone5:': '👩ðŸ¿â€ðŸŽ¨', + ':woman_astronaut_light_skin_tone:': '👩ðŸ»â€ðŸš€', + ':woman_astronaut_tone1:': '👩ðŸ»â€ðŸš€', + ':woman_astronaut_medium_light_skin_tone:': '👩ðŸ¼â€ðŸš€', + ':woman_astronaut_tone2:': '👩ðŸ¼â€ðŸš€', + ':woman_astronaut_medium_skin_tone:': '👩ðŸ½â€ðŸš€', + ':woman_astronaut_tone3:': '👩ðŸ½â€ðŸš€', + ':woman_astronaut_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸš€', + ':woman_astronaut_tone4:': '👩ðŸ¾â€ðŸš€', + ':woman_astronaut_dark_skin_tone:': '👩ðŸ¿â€ðŸš€', + ':woman_astronaut_tone5:': '👩ðŸ¿â€ðŸš€', + ':woman_bald_light_skin_tone:': '👩ðŸ»â€ðŸ¦²', + ':woman_bald_tone1:': '👩ðŸ»â€ðŸ¦²', + ':woman_bald_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦²', + ':woman_bald_tone2:': '👩ðŸ¼â€ðŸ¦²', + ':woman_bald_medium_skin_tone:': '👩ðŸ½â€ðŸ¦²', + ':woman_bald_tone3:': '👩ðŸ½â€ðŸ¦²', + ':woman_bald_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦²', + ':woman_bald_tone4:': '👩ðŸ¾â€ðŸ¦²', + ':woman_bald_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦²', + ':woman_bald_tone5:': '👩ðŸ¿â€ðŸ¦²', + ':woman_cook_light_skin_tone:': '👩ðŸ»â€ðŸ³', + ':woman_cook_tone1:': '👩ðŸ»â€ðŸ³', + ':woman_cook_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ³', + ':woman_cook_tone2:': '👩ðŸ¼â€ðŸ³', + ':woman_cook_medium_skin_tone:': '👩ðŸ½â€ðŸ³', + ':woman_cook_tone3:': '👩ðŸ½â€ðŸ³', + ':woman_cook_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ³', + ':woman_cook_tone4:': '👩ðŸ¾â€ðŸ³', + ':woman_cook_dark_skin_tone:': '👩ðŸ¿â€ðŸ³', + ':woman_cook_tone5:': '👩ðŸ¿â€ðŸ³', + ':woman_curly_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦±', + ':woman_curly_haired_tone1:': '👩ðŸ»â€ðŸ¦±', + ':woman_curly_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦±', + ':woman_curly_haired_tone2:': '👩ðŸ¼â€ðŸ¦±', + ':woman_curly_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦±', + ':woman_curly_haired_tone3:': '👩ðŸ½â€ðŸ¦±', + ':woman_curly_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦±', + ':woman_curly_haired_tone4:': '👩ðŸ¾â€ðŸ¦±', + ':woman_curly_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦±', + ':woman_curly_haired_tone5:': '👩ðŸ¿â€ðŸ¦±', + ':woman_factory_worker_light_skin_tone:': '👩ðŸ»â€ðŸ', + ':woman_factory_worker_tone1:': '👩ðŸ»â€ðŸ', + ':woman_factory_worker_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ', + ':woman_factory_worker_tone2:': '👩ðŸ¼â€ðŸ', + ':woman_factory_worker_medium_skin_tone:': '👩ðŸ½â€ðŸ', + ':woman_factory_worker_tone3:': '👩ðŸ½â€ðŸ', + ':woman_factory_worker_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ', + ':woman_factory_worker_tone4:': '👩ðŸ¾â€ðŸ', + ':woman_factory_worker_dark_skin_tone:': '👩ðŸ¿â€ðŸ', + ':woman_factory_worker_tone5:': '👩ðŸ¿â€ðŸ', + ':woman_farmer_light_skin_tone:': '👩ðŸ»â€ðŸŒ¾', + ':woman_farmer_tone1:': '👩ðŸ»â€ðŸŒ¾', + ':woman_farmer_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŒ¾', + ':woman_farmer_tone2:': '👩ðŸ¼â€ðŸŒ¾', + ':woman_farmer_medium_skin_tone:': '👩ðŸ½â€ðŸŒ¾', + ':woman_farmer_tone3:': '👩ðŸ½â€ðŸŒ¾', + ':woman_farmer_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŒ¾', + ':woman_farmer_tone4:': '👩ðŸ¾â€ðŸŒ¾', + ':woman_farmer_dark_skin_tone:': '👩ðŸ¿â€ðŸŒ¾', + ':woman_farmer_tone5:': '👩ðŸ¿â€ðŸŒ¾', + ':woman_firefighter_light_skin_tone:': '👩ðŸ»â€ðŸš’', + ':woman_firefighter_tone1:': '👩ðŸ»â€ðŸš’', + ':woman_firefighter_medium_light_skin_tone:': '👩ðŸ¼â€ðŸš’', + ':woman_firefighter_tone2:': '👩ðŸ¼â€ðŸš’', + ':woman_firefighter_medium_skin_tone:': '👩ðŸ½â€ðŸš’', + ':woman_firefighter_tone3:': '👩ðŸ½â€ðŸš’', + ':woman_firefighter_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸš’', + ':woman_firefighter_tone4:': '👩ðŸ¾â€ðŸš’', + ':woman_firefighter_dark_skin_tone:': '👩ðŸ¿â€ðŸš’', + ':woman_firefighter_tone5:': '👩ðŸ¿â€ðŸš’', + ':woman_in_manual_wheelchair_light_skin_tone:': '👩ðŸ»â€ðŸ¦½', + ':woman_in_manual_wheelchair_tone1:': '👩ðŸ»â€ðŸ¦½', + ':woman_in_manual_wheelchair_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦½', + ':woman_in_manual_wheelchair_tone2:': '👩ðŸ¼â€ðŸ¦½', + ':woman_in_manual_wheelchair_medium_skin_tone:': '👩ðŸ½â€ðŸ¦½', + ':woman_in_manual_wheelchair_tone3:': '👩ðŸ½â€ðŸ¦½', + ':woman_in_manual_wheelchair_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦½', + ':woman_in_manual_wheelchair_tone4:': '👩ðŸ¾â€ðŸ¦½', + ':woman_in_manual_wheelchair_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦½', + ':woman_in_manual_wheelchair_tone5:': '👩ðŸ¿â€ðŸ¦½', + ':woman_in_motorized_wheelchair_light_skin_tone:': '👩ðŸ»â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_tone1:': '👩ðŸ»â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_tone2:': '👩ðŸ¼â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_medium_skin_tone:': '👩ðŸ½â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_tone3:': '👩ðŸ½â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_tone4:': '👩ðŸ¾â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦¼', + ':woman_in_motorized_wheelchair_tone5:': '👩ðŸ¿â€ðŸ¦¼', + ':woman_mechanic_light_skin_tone:': '👩ðŸ»â€ðŸ”§', + ':woman_mechanic_tone1:': '👩ðŸ»â€ðŸ”§', + ':woman_mechanic_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ”§', + ':woman_mechanic_tone2:': '👩ðŸ¼â€ðŸ”§', + ':woman_mechanic_medium_skin_tone:': '👩ðŸ½â€ðŸ”§', + ':woman_mechanic_tone3:': '👩ðŸ½â€ðŸ”§', + ':woman_mechanic_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ”§', + ':woman_mechanic_tone4:': '👩ðŸ¾â€ðŸ”§', + ':woman_mechanic_dark_skin_tone:': '👩ðŸ¿â€ðŸ”§', + ':woman_mechanic_tone5:': '👩ðŸ¿â€ðŸ”§', + ':woman_office_worker_light_skin_tone:': '👩ðŸ»â€ðŸ’¼', + ':woman_office_worker_tone1:': '👩ðŸ»â€ðŸ’¼', + ':woman_office_worker_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ’¼', + ':woman_office_worker_tone2:': '👩ðŸ¼â€ðŸ’¼', + ':woman_office_worker_medium_skin_tone:': '👩ðŸ½â€ðŸ’¼', + ':woman_office_worker_tone3:': '👩ðŸ½â€ðŸ’¼', + ':woman_office_worker_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ’¼', + ':woman_office_worker_tone4:': '👩ðŸ¾â€ðŸ’¼', + ':woman_office_worker_dark_skin_tone:': '👩ðŸ¿â€ðŸ’¼', + ':woman_office_worker_tone5:': '👩ðŸ¿â€ðŸ’¼', + ':woman_red_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦°', + ':woman_red_haired_tone1:': '👩ðŸ»â€ðŸ¦°', + ':woman_red_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦°', + ':woman_red_haired_tone2:': '👩ðŸ¼â€ðŸ¦°', + ':woman_red_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦°', + ':woman_red_haired_tone3:': '👩ðŸ½â€ðŸ¦°', + ':woman_red_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦°', + ':woman_red_haired_tone4:': '👩ðŸ¾â€ðŸ¦°', + ':woman_red_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦°', + ':woman_red_haired_tone5:': '👩ðŸ¿â€ðŸ¦°', + ':woman_scientist_light_skin_tone:': '👩ðŸ»â€ðŸ”¬', + ':woman_scientist_tone1:': '👩ðŸ»â€ðŸ”¬', + ':woman_scientist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ”¬', + ':woman_scientist_tone2:': '👩ðŸ¼â€ðŸ”¬', + ':woman_scientist_medium_skin_tone:': '👩ðŸ½â€ðŸ”¬', + ':woman_scientist_tone3:': '👩ðŸ½â€ðŸ”¬', + ':woman_scientist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ”¬', + ':woman_scientist_tone4:': '👩ðŸ¾â€ðŸ”¬', + ':woman_scientist_dark_skin_tone:': '👩ðŸ¿â€ðŸ”¬', + ':woman_scientist_tone5:': '👩ðŸ¿â€ðŸ”¬', + ':woman_singer_light_skin_tone:': '👩ðŸ»â€ðŸŽ¤', + ':woman_singer_tone1:': '👩ðŸ»â€ðŸŽ¤', + ':woman_singer_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ¤', + ':woman_singer_tone2:': '👩ðŸ¼â€ðŸŽ¤', + ':woman_singer_medium_skin_tone:': '👩ðŸ½â€ðŸŽ¤', + ':woman_singer_tone3:': '👩ðŸ½â€ðŸŽ¤', + ':woman_singer_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ¤', + ':woman_singer_tone4:': '👩ðŸ¾â€ðŸŽ¤', + ':woman_singer_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ¤', + ':woman_singer_tone5:': '👩ðŸ¿â€ðŸŽ¤', + ':woman_student_light_skin_tone:': '👩ðŸ»â€ðŸŽ“', + ':woman_student_tone1:': '👩ðŸ»â€ðŸŽ“', + ':woman_student_medium_light_skin_tone:': '👩ðŸ¼â€ðŸŽ“', + ':woman_student_tone2:': '👩ðŸ¼â€ðŸŽ“', + ':woman_student_medium_skin_tone:': '👩ðŸ½â€ðŸŽ“', + ':woman_student_tone3:': '👩ðŸ½â€ðŸŽ“', + ':woman_student_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸŽ“', + ':woman_student_tone4:': '👩ðŸ¾â€ðŸŽ“', + ':woman_student_dark_skin_tone:': '👩ðŸ¿â€ðŸŽ“', + ':woman_student_tone5:': '👩ðŸ¿â€ðŸŽ“', + ':woman_teacher_light_skin_tone:': '👩ðŸ»â€ðŸ«', + ':woman_teacher_tone1:': '👩ðŸ»â€ðŸ«', + ':woman_teacher_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ«', + ':woman_teacher_tone2:': '👩ðŸ¼â€ðŸ«', + ':woman_teacher_medium_skin_tone:': '👩ðŸ½â€ðŸ«', + ':woman_teacher_tone3:': '👩ðŸ½â€ðŸ«', + ':woman_teacher_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ«', + ':woman_teacher_tone4:': '👩ðŸ¾â€ðŸ«', + ':woman_teacher_dark_skin_tone:': '👩ðŸ¿â€ðŸ«', + ':woman_teacher_tone5:': '👩ðŸ¿â€ðŸ«', + ':woman_technologist_light_skin_tone:': '👩ðŸ»â€ðŸ’»', + ':woman_technologist_tone1:': '👩ðŸ»â€ðŸ’»', + ':woman_technologist_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ’»', + ':woman_technologist_tone2:': '👩ðŸ¼â€ðŸ’»', + ':woman_technologist_medium_skin_tone:': '👩ðŸ½â€ðŸ’»', + ':woman_technologist_tone3:': '👩ðŸ½â€ðŸ’»', + ':woman_technologist_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ’»', + ':woman_technologist_tone4:': '👩ðŸ¾â€ðŸ’»', + ':woman_technologist_dark_skin_tone:': '👩ðŸ¿â€ðŸ’»', + ':woman_technologist_tone5:': '👩ðŸ¿â€ðŸ’»', + ':woman_white_haired_light_skin_tone:': '👩ðŸ»â€ðŸ¦³', + ':woman_white_haired_tone1:': '👩ðŸ»â€ðŸ¦³', + ':woman_white_haired_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦³', + ':woman_white_haired_tone2:': '👩ðŸ¼â€ðŸ¦³', + ':woman_white_haired_medium_skin_tone:': '👩ðŸ½â€ðŸ¦³', + ':woman_white_haired_tone3:': '👩ðŸ½â€ðŸ¦³', + ':woman_white_haired_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦³', + ':woman_white_haired_tone4:': '👩ðŸ¾â€ðŸ¦³', + ':woman_white_haired_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦³', + ':woman_white_haired_tone5:': '👩ðŸ¿â€ðŸ¦³', + ':woman_with_probing_cane_light_skin_tone:': '👩ðŸ»â€ðŸ¦¯', + ':woman_with_probing_cane_tone1:': '👩ðŸ»â€ðŸ¦¯', + ':woman_with_probing_cane_medium_light_skin_tone:': '👩ðŸ¼â€ðŸ¦¯', + ':woman_with_probing_cane_tone2:': '👩ðŸ¼â€ðŸ¦¯', + ':woman_with_probing_cane_medium_skin_tone:': '👩ðŸ½â€ðŸ¦¯', + ':woman_with_probing_cane_tone3:': '👩ðŸ½â€ðŸ¦¯', + ':woman_with_probing_cane_medium_dark_skin_tone:': '👩ðŸ¾â€ðŸ¦¯', + ':woman_with_probing_cane_tone4:': '👩ðŸ¾â€ðŸ¦¯', + ':woman_with_probing_cane_dark_skin_tone:': '👩ðŸ¿â€ðŸ¦¯', + ':woman_with_probing_cane_tone5:': '👩ðŸ¿â€ðŸ¦¯', + ':blond-haired_man_light_skin_tone:': '👱ðŸ»â€â™‚ï¸', + ':blond-haired_man_tone1:': '👱ðŸ»â€â™‚ï¸', + ':blond-haired_man_medium_light_skin_tone:': '👱ðŸ¼â€â™‚ï¸', + ':blond-haired_man_tone2:': '👱ðŸ¼â€â™‚ï¸', + ':blond-haired_man_medium_skin_tone:': '👱ðŸ½â€â™‚ï¸', + ':blond-haired_man_tone3:': '👱ðŸ½â€â™‚ï¸', + ':blond-haired_man_medium_dark_skin_tone:': '👱ðŸ¾â€â™‚ï¸', + ':blond-haired_man_tone4:': '👱ðŸ¾â€â™‚ï¸', + ':blond-haired_man_dark_skin_tone:': '👱ðŸ¿â€â™‚ï¸', + ':blond-haired_man_tone5:': '👱ðŸ¿â€â™‚ï¸', + ':blond-haired_woman_light_skin_tone:': '👱ðŸ»â€â™€ï¸', + ':blond-haired_woman_tone1:': '👱ðŸ»â€â™€ï¸', + ':blond-haired_woman_medium_light_skin_tone:': '👱ðŸ¼â€â™€ï¸', + ':blond-haired_woman_tone2:': '👱ðŸ¼â€â™€ï¸', + ':blond-haired_woman_medium_skin_tone:': '👱ðŸ½â€â™€ï¸', + ':blond-haired_woman_tone3:': '👱ðŸ½â€â™€ï¸', + ':blond-haired_woman_medium_dark_skin_tone:': '👱ðŸ¾â€â™€ï¸', + ':blond-haired_woman_tone4:': '👱ðŸ¾â€â™€ï¸', + ':blond-haired_woman_dark_skin_tone:': '👱ðŸ¿â€â™€ï¸', + ':blond-haired_woman_tone5:': '👱ðŸ¿â€â™€ï¸', + ':couple_with_heart_mm:': '👨â€â¤ï¸â€ðŸ‘¨', + ':couple_mm:': '👨â€â¤ï¸â€ðŸ‘¨', + ':couple_with_heart_woman_man:': '👩â€â¤ï¸â€ðŸ‘¨', + ':couple_with_heart_ww:': '👩â€â¤ï¸â€ðŸ‘©', + ':couple_ww:': '👩â€â¤ï¸â€ðŸ‘©', + ':deaf_man_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':deaf_man_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':deaf_man_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':deaf_man_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':deaf_man_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':deaf_man_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':deaf_man_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':deaf_man_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':deaf_man_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':deaf_man_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':deaf_woman_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', + ':deaf_woman_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', + ':deaf_woman_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':deaf_woman_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':deaf_woman_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', + ':deaf_woman_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', + ':deaf_woman_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':deaf_woman_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':deaf_woman_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':deaf_woman_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':man_biking_light_skin_tone:': '🚴ðŸ»â€â™‚ï¸', + ':man_biking_tone1:': '🚴ðŸ»â€â™‚ï¸', + ':man_biking_medium_light_skin_tone:': '🚴ðŸ¼â€â™‚ï¸', + ':man_biking_tone2:': '🚴ðŸ¼â€â™‚ï¸', + ':man_biking_medium_skin_tone:': '🚴ðŸ½â€â™‚ï¸', + ':man_biking_tone3:': '🚴ðŸ½â€â™‚ï¸', + ':man_biking_medium_dark_skin_tone:': '🚴ðŸ¾â€â™‚ï¸', + ':man_biking_tone4:': '🚴ðŸ¾â€â™‚ï¸', + ':man_biking_dark_skin_tone:': '🚴ðŸ¿â€â™‚ï¸', + ':man_biking_tone5:': '🚴ðŸ¿â€â™‚ï¸', + ':man_bowing_light_skin_tone:': '🙇ðŸ»â€â™‚ï¸', + ':man_bowing_tone1:': '🙇ðŸ»â€â™‚ï¸', + ':man_bowing_medium_light_skin_tone:': '🙇ðŸ¼â€â™‚ï¸', + ':man_bowing_tone2:': '🙇ðŸ¼â€â™‚ï¸', + ':man_bowing_medium_skin_tone:': '🙇ðŸ½â€â™‚ï¸', + ':man_bowing_tone3:': '🙇ðŸ½â€â™‚ï¸', + ':man_bowing_medium_dark_skin_tone:': '🙇ðŸ¾â€â™‚ï¸', + ':man_bowing_tone4:': '🙇ðŸ¾â€â™‚ï¸', + ':man_bowing_dark_skin_tone:': '🙇ðŸ¿â€â™‚ï¸', + ':man_bowing_tone5:': '🙇ðŸ¿â€â™‚ï¸', + ':man_cartwheeling_light_skin_tone:': '🤸ðŸ»â€â™‚ï¸', + ':man_cartwheeling_tone1:': '🤸ðŸ»â€â™‚ï¸', + ':man_cartwheeling_medium_light_skin_tone:': '🤸ðŸ¼â€â™‚ï¸', + ':man_cartwheeling_tone2:': '🤸ðŸ¼â€â™‚ï¸', + ':man_cartwheeling_medium_skin_tone:': '🤸ðŸ½â€â™‚ï¸', + ':man_cartwheeling_tone3:': '🤸ðŸ½â€â™‚ï¸', + ':man_cartwheeling_medium_dark_skin_tone:': '🤸ðŸ¾â€â™‚ï¸', + ':man_cartwheeling_tone4:': '🤸ðŸ¾â€â™‚ï¸', + ':man_cartwheeling_dark_skin_tone:': '🤸ðŸ¿â€â™‚ï¸', + ':man_cartwheeling_tone5:': '🤸ðŸ¿â€â™‚ï¸', + ':man_climbing_light_skin_tone:': '🧗ðŸ»â€â™‚ï¸', + ':man_climbing_tone1:': '🧗ðŸ»â€â™‚ï¸', + ':man_climbing_medium_light_skin_tone:': '🧗ðŸ¼â€â™‚ï¸', + ':man_climbing_tone2:': '🧗ðŸ¼â€â™‚ï¸', + ':man_climbing_medium_skin_tone:': '🧗ðŸ½â€â™‚ï¸', + ':man_climbing_tone3:': '🧗ðŸ½â€â™‚ï¸', + ':man_climbing_medium_dark_skin_tone:': '🧗ðŸ¾â€â™‚ï¸', + ':man_climbing_tone4:': '🧗ðŸ¾â€â™‚ï¸', + ':man_climbing_dark_skin_tone:': '🧗ðŸ¿â€â™‚ï¸', + ':man_climbing_tone5:': '🧗ðŸ¿â€â™‚ï¸', + ':man_construction_worker_light_skin_tone:': '👷ðŸ»â€â™‚ï¸', + ':man_construction_worker_tone1:': '👷ðŸ»â€â™‚ï¸', + ':man_construction_worker_medium_light_skin_tone:': '👷ðŸ¼â€â™‚ï¸', + ':man_construction_worker_tone2:': '👷ðŸ¼â€â™‚ï¸', + ':man_construction_worker_medium_skin_tone:': '👷ðŸ½â€â™‚ï¸', + ':man_construction_worker_tone3:': '👷ðŸ½â€â™‚ï¸', + ':man_construction_worker_medium_dark_skin_tone:': '👷ðŸ¾â€â™‚ï¸', + ':man_construction_worker_tone4:': '👷ðŸ¾â€â™‚ï¸', + ':man_construction_worker_dark_skin_tone:': '👷ðŸ¿â€â™‚ï¸', + ':man_construction_worker_tone5:': '👷ðŸ¿â€â™‚ï¸', + ':man_detective_light_skin_tone:': '🕵ï¸ðŸ»â€â™‚ï¸', + ':man_detective_tone1:': '🕵ï¸ðŸ»â€â™‚ï¸', + ':man_detective_medium_light_skin_tone:': '🕵ï¸ðŸ¼â€â™‚ï¸', + ':man_detective_tone2:': '🕵ï¸ðŸ¼â€â™‚ï¸', + ':man_detective_medium_skin_tone:': '🕵ï¸ðŸ½â€â™‚ï¸', + ':man_detective_tone3:': '🕵ï¸ðŸ½â€â™‚ï¸', + ':man_detective_medium_dark_skin_tone:': '🕵ï¸ðŸ¾â€â™‚ï¸', + ':man_detective_tone4:': '🕵ï¸ðŸ¾â€â™‚ï¸', + ':man_detective_dark_skin_tone:': '🕵ï¸ðŸ¿â€â™‚ï¸', + ':man_detective_tone5:': '🕵ï¸ðŸ¿â€â™‚ï¸', + ':man_elf_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':man_elf_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':man_elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':man_elf_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':man_elf_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':man_elf_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':man_elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':man_elf_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':man_elf_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':man_elf_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':man_facepalming_light_skin_tone:': '🤦ðŸ»â€â™‚ï¸', + ':man_facepalming_tone1:': '🤦ðŸ»â€â™‚ï¸', + ':man_facepalming_medium_light_skin_tone:': '🤦ðŸ¼â€â™‚ï¸', + ':man_facepalming_tone2:': '🤦ðŸ¼â€â™‚ï¸', + ':man_facepalming_medium_skin_tone:': '🤦ðŸ½â€â™‚ï¸', + ':man_facepalming_tone3:': '🤦ðŸ½â€â™‚ï¸', + ':man_facepalming_medium_dark_skin_tone:': '🤦ðŸ¾â€â™‚ï¸', + ':man_facepalming_tone4:': '🤦ðŸ¾â€â™‚ï¸', + ':man_facepalming_dark_skin_tone:': '🤦ðŸ¿â€â™‚ï¸', + ':man_facepalming_tone5:': '🤦ðŸ¿â€â™‚ï¸', + ':man_fairy_light_skin_tone:': '🧚ðŸ»â€â™‚ï¸', + ':man_fairy_tone1:': '🧚ðŸ»â€â™‚ï¸', + ':man_fairy_medium_light_skin_tone:': '🧚ðŸ¼â€â™‚ï¸', + ':man_fairy_tone2:': '🧚ðŸ¼â€â™‚ï¸', + ':man_fairy_medium_skin_tone:': '🧚ðŸ½â€â™‚ï¸', + ':man_fairy_tone3:': '🧚ðŸ½â€â™‚ï¸', + ':man_fairy_medium_dark_skin_tone:': '🧚ðŸ¾â€â™‚ï¸', + ':man_fairy_tone4:': '🧚ðŸ¾â€â™‚ï¸', + ':man_fairy_dark_skin_tone:': '🧚ðŸ¿â€â™‚ï¸', + ':man_fairy_tone5:': '🧚ðŸ¿â€â™‚ï¸', + ':man_frowning_light_skin_tone:': 'ðŸ™ðŸ»â€â™‚ï¸', + ':man_frowning_tone1:': 'ðŸ™ðŸ»â€â™‚ï¸', + ':man_frowning_medium_light_skin_tone:': 'ðŸ™ðŸ¼â€â™‚ï¸', + ':man_frowning_tone2:': 'ðŸ™ðŸ¼â€â™‚ï¸', + ':man_frowning_medium_skin_tone:': 'ðŸ™ðŸ½â€â™‚ï¸', + ':man_frowning_tone3:': 'ðŸ™ðŸ½â€â™‚ï¸', + ':man_frowning_medium_dark_skin_tone:': 'ðŸ™ðŸ¾â€â™‚ï¸', + ':man_frowning_tone4:': 'ðŸ™ðŸ¾â€â™‚ï¸', + ':man_frowning_dark_skin_tone:': 'ðŸ™ðŸ¿â€â™‚ï¸', + ':man_frowning_tone5:': 'ðŸ™ðŸ¿â€â™‚ï¸', + ':man_gesturing_no_light_skin_tone:': '🙅ðŸ»â€â™‚ï¸', + ':man_gesturing_no_tone1:': '🙅ðŸ»â€â™‚ï¸', + ':man_gesturing_no_medium_light_skin_tone:': '🙅ðŸ¼â€â™‚ï¸', + ':man_gesturing_no_tone2:': '🙅ðŸ¼â€â™‚ï¸', + ':man_gesturing_no_medium_skin_tone:': '🙅ðŸ½â€â™‚ï¸', + ':man_gesturing_no_tone3:': '🙅ðŸ½â€â™‚ï¸', + ':man_gesturing_no_medium_dark_skin_tone:': '🙅ðŸ¾â€â™‚ï¸', + ':man_gesturing_no_tone4:': '🙅ðŸ¾â€â™‚ï¸', + ':man_gesturing_no_dark_skin_tone:': '🙅ðŸ¿â€â™‚ï¸', + ':man_gesturing_no_tone5:': '🙅ðŸ¿â€â™‚ï¸', + ':man_gesturing_ok_light_skin_tone:': '🙆ðŸ»â€â™‚ï¸', + ':man_gesturing_ok_tone1:': '🙆ðŸ»â€â™‚ï¸', + ':man_gesturing_ok_medium_light_skin_tone:': '🙆ðŸ¼â€â™‚ï¸', + ':man_gesturing_ok_tone2:': '🙆ðŸ¼â€â™‚ï¸', + ':man_gesturing_ok_medium_skin_tone:': '🙆ðŸ½â€â™‚ï¸', + ':man_gesturing_ok_tone3:': '🙆ðŸ½â€â™‚ï¸', + ':man_gesturing_ok_medium_dark_skin_tone:': '🙆ðŸ¾â€â™‚ï¸', + ':man_gesturing_ok_tone4:': '🙆ðŸ¾â€â™‚ï¸', + ':man_gesturing_ok_dark_skin_tone:': '🙆ðŸ¿â€â™‚ï¸', + ':man_gesturing_ok_tone5:': '🙆ðŸ¿â€â™‚ï¸', + ':man_getting_face_massage_light_skin_tone:': '💆ðŸ»â€â™‚ï¸', + ':man_getting_face_massage_tone1:': '💆ðŸ»â€â™‚ï¸', + ':man_getting_face_massage_medium_light_skin_tone:': '💆ðŸ¼â€â™‚ï¸', + ':man_getting_face_massage_tone2:': '💆ðŸ¼â€â™‚ï¸', + ':man_getting_face_massage_medium_skin_tone:': '💆ðŸ½â€â™‚ï¸', + ':man_getting_face_massage_tone3:': '💆ðŸ½â€â™‚ï¸', + ':man_getting_face_massage_medium_dark_skin_tone:': '💆ðŸ¾â€â™‚ï¸', + ':man_getting_face_massage_tone4:': '💆ðŸ¾â€â™‚ï¸', + ':man_getting_face_massage_dark_skin_tone:': '💆ðŸ¿â€â™‚ï¸', + ':man_getting_face_massage_tone5:': '💆ðŸ¿â€â™‚ï¸', + ':man_getting_haircut_light_skin_tone:': '💇ðŸ»â€â™‚ï¸', + ':man_getting_haircut_tone1:': '💇ðŸ»â€â™‚ï¸', + ':man_getting_haircut_medium_light_skin_tone:': '💇ðŸ¼â€â™‚ï¸', + ':man_getting_haircut_tone2:': '💇ðŸ¼â€â™‚ï¸', + ':man_getting_haircut_medium_skin_tone:': '💇ðŸ½â€â™‚ï¸', + ':man_getting_haircut_tone3:': '💇ðŸ½â€â™‚ï¸', + ':man_getting_haircut_medium_dark_skin_tone:': '💇ðŸ¾â€â™‚ï¸', + ':man_getting_haircut_tone4:': '💇ðŸ¾â€â™‚ï¸', + ':man_getting_haircut_dark_skin_tone:': '💇ðŸ¿â€â™‚ï¸', + ':man_getting_haircut_tone5:': '💇ðŸ¿â€â™‚ï¸', + ':man_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»â€â™‚ï¸', + ':man_golfing_tone1:': 'ðŸŒï¸ðŸ»â€â™‚ï¸', + ':man_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼â€â™‚ï¸', + ':man_golfing_tone2:': 'ðŸŒï¸ðŸ¼â€â™‚ï¸', + ':man_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½â€â™‚ï¸', + ':man_golfing_tone3:': 'ðŸŒï¸ðŸ½â€â™‚ï¸', + ':man_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾â€â™‚ï¸', + ':man_golfing_tone4:': 'ðŸŒï¸ðŸ¾â€â™‚ï¸', + ':man_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿â€â™‚ï¸', + ':man_golfing_tone5:': 'ðŸŒï¸ðŸ¿â€â™‚ï¸', + ':man_guard_light_skin_tone:': '💂ðŸ»â€â™‚ï¸', + ':man_guard_tone1:': '💂ðŸ»â€â™‚ï¸', + ':man_guard_medium_light_skin_tone:': '💂ðŸ¼â€â™‚ï¸', + ':man_guard_tone2:': '💂ðŸ¼â€â™‚ï¸', + ':man_guard_medium_skin_tone:': '💂ðŸ½â€â™‚ï¸', + ':man_guard_tone3:': '💂ðŸ½â€â™‚ï¸', + ':man_guard_medium_dark_skin_tone:': '💂ðŸ¾â€â™‚ï¸', + ':man_guard_tone4:': '💂ðŸ¾â€â™‚ï¸', + ':man_guard_dark_skin_tone:': '💂ðŸ¿â€â™‚ï¸', + ':man_guard_tone5:': '💂ðŸ¿â€â™‚ï¸', + ':man_health_worker_light_skin_tone:': '👨ðŸ»â€âš•ï¸', + ':man_health_worker_tone1:': '👨ðŸ»â€âš•ï¸', + ':man_health_worker_medium_light_skin_tone:': '👨ðŸ¼â€âš•ï¸', + ':man_health_worker_tone2:': '👨ðŸ¼â€âš•ï¸', + ':man_health_worker_medium_skin_tone:': '👨ðŸ½â€âš•ï¸', + ':man_health_worker_tone3:': '👨ðŸ½â€âš•ï¸', + ':man_health_worker_medium_dark_skin_tone:': '👨ðŸ¾â€âš•ï¸', + ':man_health_worker_tone4:': '👨ðŸ¾â€âš•ï¸', + ':man_health_worker_dark_skin_tone:': '👨ðŸ¿â€âš•ï¸', + ':man_health_worker_tone5:': '👨ðŸ¿â€âš•ï¸', + ':man_in_lotus_position_light_skin_tone:': '🧘ðŸ»â€â™‚ï¸', + ':man_in_lotus_position_tone1:': '🧘ðŸ»â€â™‚ï¸', + ':man_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼â€â™‚ï¸', + ':man_in_lotus_position_tone2:': '🧘ðŸ¼â€â™‚ï¸', + ':man_in_lotus_position_medium_skin_tone:': '🧘ðŸ½â€â™‚ï¸', + ':man_in_lotus_position_tone3:': '🧘ðŸ½â€â™‚ï¸', + ':man_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾â€â™‚ï¸', + ':man_in_lotus_position_tone4:': '🧘ðŸ¾â€â™‚ï¸', + ':man_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿â€â™‚ï¸', + ':man_in_lotus_position_tone5:': '🧘ðŸ¿â€â™‚ï¸', + ':man_in_steamy_room_light_skin_tone:': '🧖ðŸ»â€â™‚ï¸', + ':man_in_steamy_room_tone1:': '🧖ðŸ»â€â™‚ï¸', + ':man_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼â€â™‚ï¸', + ':man_in_steamy_room_tone2:': '🧖ðŸ¼â€â™‚ï¸', + ':man_in_steamy_room_medium_skin_tone:': '🧖ðŸ½â€â™‚ï¸', + ':man_in_steamy_room_tone3:': '🧖ðŸ½â€â™‚ï¸', + ':man_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾â€â™‚ï¸', + ':man_in_steamy_room_tone4:': '🧖ðŸ¾â€â™‚ï¸', + ':man_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿â€â™‚ï¸', + ':man_in_steamy_room_tone5:': '🧖ðŸ¿â€â™‚ï¸', + ':man_judge_light_skin_tone:': '👨ðŸ»â€âš–ï¸', + ':man_judge_tone1:': '👨ðŸ»â€âš–ï¸', + ':man_judge_medium_light_skin_tone:': '👨ðŸ¼â€âš–ï¸', + ':man_judge_tone2:': '👨ðŸ¼â€âš–ï¸', + ':man_judge_medium_skin_tone:': '👨ðŸ½â€âš–ï¸', + ':man_judge_tone3:': '👨ðŸ½â€âš–ï¸', + ':man_judge_medium_dark_skin_tone:': '👨ðŸ¾â€âš–ï¸', + ':man_judge_tone4:': '👨ðŸ¾â€âš–ï¸', + ':man_judge_dark_skin_tone:': '👨ðŸ¿â€âš–ï¸', + ':man_judge_tone5:': '👨ðŸ¿â€âš–ï¸', + ':man_juggling_light_skin_tone:': '🤹ðŸ»â€â™‚ï¸', + ':man_juggling_tone1:': '🤹ðŸ»â€â™‚ï¸', + ':man_juggling_medium_light_skin_tone:': '🤹ðŸ¼â€â™‚ï¸', + ':man_juggling_tone2:': '🤹ðŸ¼â€â™‚ï¸', + ':man_juggling_medium_skin_tone:': '🤹ðŸ½â€â™‚ï¸', + ':man_juggling_tone3:': '🤹ðŸ½â€â™‚ï¸', + ':man_juggling_medium_dark_skin_tone:': '🤹ðŸ¾â€â™‚ï¸', + ':man_juggling_tone4:': '🤹ðŸ¾â€â™‚ï¸', + ':man_juggling_dark_skin_tone:': '🤹ðŸ¿â€â™‚ï¸', + ':man_juggling_tone5:': '🤹ðŸ¿â€â™‚ï¸', + ':man_kneeling_light_skin_tone:': '🧎ðŸ»â€â™‚ï¸', + ':man_kneeling_tone1:': '🧎ðŸ»â€â™‚ï¸', + ':man_kneeling_medium_light_skin_tone:': '🧎ðŸ¼â€â™‚ï¸', + ':man_kneeling_tone2:': '🧎ðŸ¼â€â™‚ï¸', + ':man_kneeling_medium_skin_tone:': '🧎ðŸ½â€â™‚ï¸', + ':man_kneeling_tone3:': '🧎ðŸ½â€â™‚ï¸', + ':man_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾â€â™‚ï¸', + ':man_kneeling_tone4:': '🧎ðŸ¾â€â™‚ï¸', + ':man_kneeling_dark_skin_tone:': '🧎ðŸ¿â€â™‚ï¸', + ':man_kneeling_tone5:': '🧎ðŸ¿â€â™‚ï¸', + ':man_lifting_weights_light_skin_tone:': 'ðŸ‹ï¸ðŸ»â€â™‚ï¸', + ':man_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»â€â™‚ï¸', + ':man_lifting_weights_medium_light_skin_tone:': 'ðŸ‹ï¸ðŸ¼â€â™‚ï¸', + ':man_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼â€â™‚ï¸', + ':man_lifting_weights_medium_skin_tone:': 'ðŸ‹ï¸ðŸ½â€â™‚ï¸', + ':man_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½â€â™‚ï¸', + ':man_lifting_weights_medium_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¾â€â™‚ï¸', + ':man_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾â€â™‚ï¸', + ':man_lifting_weights_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¿â€â™‚ï¸', + ':man_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿â€â™‚ï¸', + ':man_mage_light_skin_tone:': '🧙ðŸ»â€â™‚ï¸', + ':man_mage_tone1:': '🧙ðŸ»â€â™‚ï¸', + ':man_mage_medium_light_skin_tone:': '🧙ðŸ¼â€â™‚ï¸', + ':man_mage_tone2:': '🧙ðŸ¼â€â™‚ï¸', + ':man_mage_medium_skin_tone:': '🧙ðŸ½â€â™‚ï¸', + ':man_mage_tone3:': '🧙ðŸ½â€â™‚ï¸', + ':man_mage_medium_dark_skin_tone:': '🧙ðŸ¾â€â™‚ï¸', + ':man_mage_tone4:': '🧙ðŸ¾â€â™‚ï¸', + ':man_mage_dark_skin_tone:': '🧙ðŸ¿â€â™‚ï¸', + ':man_mage_tone5:': '🧙ðŸ¿â€â™‚ï¸', + ':man_mountain_biking_light_skin_tone:': '🚵ðŸ»â€â™‚ï¸', + ':man_mountain_biking_tone1:': '🚵ðŸ»â€â™‚ï¸', + ':man_mountain_biking_medium_light_skin_tone:': '🚵ðŸ¼â€â™‚ï¸', + ':man_mountain_biking_tone2:': '🚵ðŸ¼â€â™‚ï¸', + ':man_mountain_biking_medium_skin_tone:': '🚵ðŸ½â€â™‚ï¸', + ':man_mountain_biking_tone3:': '🚵ðŸ½â€â™‚ï¸', + ':man_mountain_biking_medium_dark_skin_tone:': '🚵ðŸ¾â€â™‚ï¸', + ':man_mountain_biking_tone4:': '🚵ðŸ¾â€â™‚ï¸', + ':man_mountain_biking_dark_skin_tone:': '🚵ðŸ¿â€â™‚ï¸', + ':man_mountain_biking_tone5:': '🚵ðŸ¿â€â™‚ï¸', + ':man_pilot_light_skin_tone:': '👨ðŸ»â€âœˆï¸', + ':man_pilot_tone1:': '👨ðŸ»â€âœˆï¸', + ':man_pilot_medium_light_skin_tone:': '👨ðŸ¼â€âœˆï¸', + ':man_pilot_tone2:': '👨ðŸ¼â€âœˆï¸', + ':man_pilot_medium_skin_tone:': '👨ðŸ½â€âœˆï¸', + ':man_pilot_tone3:': '👨ðŸ½â€âœˆï¸', + ':man_pilot_medium_dark_skin_tone:': '👨ðŸ¾â€âœˆï¸', + ':man_pilot_tone4:': '👨ðŸ¾â€âœˆï¸', + ':man_pilot_dark_skin_tone:': '👨ðŸ¿â€âœˆï¸', + ':man_pilot_tone5:': '👨ðŸ¿â€âœˆï¸', + ':man_playing_handball_light_skin_tone:': '🤾ðŸ»â€â™‚ï¸', + ':man_playing_handball_tone1:': '🤾ðŸ»â€â™‚ï¸', + ':man_playing_handball_medium_light_skin_tone:': '🤾ðŸ¼â€â™‚ï¸', + ':man_playing_handball_tone2:': '🤾ðŸ¼â€â™‚ï¸', + ':man_playing_handball_medium_skin_tone:': '🤾ðŸ½â€â™‚ï¸', + ':man_playing_handball_tone3:': '🤾ðŸ½â€â™‚ï¸', + ':man_playing_handball_medium_dark_skin_tone:': '🤾ðŸ¾â€â™‚ï¸', + ':man_playing_handball_tone4:': '🤾ðŸ¾â€â™‚ï¸', + ':man_playing_handball_dark_skin_tone:': '🤾ðŸ¿â€â™‚ï¸', + ':man_playing_handball_tone5:': '🤾ðŸ¿â€â™‚ï¸', + ':man_playing_water_polo_light_skin_tone:': '🤽ðŸ»â€â™‚ï¸', + ':man_playing_water_polo_tone1:': '🤽ðŸ»â€â™‚ï¸', + ':man_playing_water_polo_medium_light_skin_tone:': '🤽ðŸ¼â€â™‚ï¸', + ':man_playing_water_polo_tone2:': '🤽ðŸ¼â€â™‚ï¸', + ':man_playing_water_polo_medium_skin_tone:': '🤽ðŸ½â€â™‚ï¸', + ':man_playing_water_polo_tone3:': '🤽ðŸ½â€â™‚ï¸', + ':man_playing_water_polo_medium_dark_skin_tone:': '🤽ðŸ¾â€â™‚ï¸', + ':man_playing_water_polo_tone4:': '🤽ðŸ¾â€â™‚ï¸', + ':man_playing_water_polo_dark_skin_tone:': '🤽ðŸ¿â€â™‚ï¸', + ':man_playing_water_polo_tone5:': '🤽ðŸ¿â€â™‚ï¸', + ':man_police_officer_light_skin_tone:': '👮ðŸ»â€â™‚ï¸', + ':man_police_officer_tone1:': '👮ðŸ»â€â™‚ï¸', + ':man_police_officer_medium_light_skin_tone:': '👮ðŸ¼â€â™‚ï¸', + ':man_police_officer_tone2:': '👮ðŸ¼â€â™‚ï¸', + ':man_police_officer_medium_skin_tone:': '👮ðŸ½â€â™‚ï¸', + ':man_police_officer_tone3:': '👮ðŸ½â€â™‚ï¸', + ':man_police_officer_medium_dark_skin_tone:': '👮ðŸ¾â€â™‚ï¸', + ':man_police_officer_tone4:': '👮ðŸ¾â€â™‚ï¸', + ':man_police_officer_dark_skin_tone:': '👮ðŸ¿â€â™‚ï¸', + ':man_police_officer_tone5:': '👮ðŸ¿â€â™‚ï¸', + ':man_pouting_light_skin_tone:': '🙎ðŸ»â€â™‚ï¸', + ':man_pouting_tone1:': '🙎ðŸ»â€â™‚ï¸', + ':man_pouting_medium_light_skin_tone:': '🙎ðŸ¼â€â™‚ï¸', + ':man_pouting_tone2:': '🙎ðŸ¼â€â™‚ï¸', + ':man_pouting_medium_skin_tone:': '🙎ðŸ½â€â™‚ï¸', + ':man_pouting_tone3:': '🙎ðŸ½â€â™‚ï¸', + ':man_pouting_medium_dark_skin_tone:': '🙎ðŸ¾â€â™‚ï¸', + ':man_pouting_tone4:': '🙎ðŸ¾â€â™‚ï¸', + ':man_pouting_dark_skin_tone:': '🙎ðŸ¿â€â™‚ï¸', + ':man_pouting_tone5:': '🙎ðŸ¿â€â™‚ï¸', + ':man_raising_hand_light_skin_tone:': '🙋ðŸ»â€â™‚ï¸', + ':man_raising_hand_tone1:': '🙋ðŸ»â€â™‚ï¸', + ':man_raising_hand_medium_light_skin_tone:': '🙋ðŸ¼â€â™‚ï¸', + ':man_raising_hand_tone2:': '🙋ðŸ¼â€â™‚ï¸', + ':man_raising_hand_medium_skin_tone:': '🙋ðŸ½â€â™‚ï¸', + ':man_raising_hand_tone3:': '🙋ðŸ½â€â™‚ï¸', + ':man_raising_hand_medium_dark_skin_tone:': '🙋ðŸ¾â€â™‚ï¸', + ':man_raising_hand_tone4:': '🙋ðŸ¾â€â™‚ï¸', + ':man_raising_hand_dark_skin_tone:': '🙋ðŸ¿â€â™‚ï¸', + ':man_raising_hand_tone5:': '🙋ðŸ¿â€â™‚ï¸', + ':man_rowing_boat_light_skin_tone:': '🚣ðŸ»â€â™‚ï¸', + ':man_rowing_boat_tone1:': '🚣ðŸ»â€â™‚ï¸', + ':man_rowing_boat_medium_light_skin_tone:': '🚣ðŸ¼â€â™‚ï¸', + ':man_rowing_boat_tone2:': '🚣ðŸ¼â€â™‚ï¸', + ':man_rowing_boat_medium_skin_tone:': '🚣ðŸ½â€â™‚ï¸', + ':man_rowing_boat_tone3:': '🚣ðŸ½â€â™‚ï¸', + ':man_rowing_boat_medium_dark_skin_tone:': '🚣ðŸ¾â€â™‚ï¸', + ':man_rowing_boat_tone4:': '🚣ðŸ¾â€â™‚ï¸', + ':man_rowing_boat_dark_skin_tone:': '🚣ðŸ¿â€â™‚ï¸', + ':man_rowing_boat_tone5:': '🚣ðŸ¿â€â™‚ï¸', + ':man_running_light_skin_tone:': 'ðŸƒðŸ»â€â™‚ï¸', + ':man_running_tone1:': 'ðŸƒðŸ»â€â™‚ï¸', + ':man_running_medium_light_skin_tone:': 'ðŸƒðŸ¼â€â™‚ï¸', + ':man_running_tone2:': 'ðŸƒðŸ¼â€â™‚ï¸', + ':man_running_medium_skin_tone:': 'ðŸƒðŸ½â€â™‚ï¸', + ':man_running_tone3:': 'ðŸƒðŸ½â€â™‚ï¸', + ':man_running_medium_dark_skin_tone:': 'ðŸƒðŸ¾â€â™‚ï¸', + ':man_running_tone4:': 'ðŸƒðŸ¾â€â™‚ï¸', + ':man_running_dark_skin_tone:': 'ðŸƒðŸ¿â€â™‚ï¸', + ':man_running_tone5:': 'ðŸƒðŸ¿â€â™‚ï¸', + ':man_shrugging_light_skin_tone:': '🤷ðŸ»â€â™‚ï¸', + ':man_shrugging_tone1:': '🤷ðŸ»â€â™‚ï¸', + ':man_shrugging_medium_light_skin_tone:': '🤷ðŸ¼â€â™‚ï¸', + ':man_shrugging_tone2:': '🤷ðŸ¼â€â™‚ï¸', + ':man_shrugging_medium_skin_tone:': '🤷ðŸ½â€â™‚ï¸', + ':man_shrugging_tone3:': '🤷ðŸ½â€â™‚ï¸', + ':man_shrugging_medium_dark_skin_tone:': '🤷ðŸ¾â€â™‚ï¸', + ':man_shrugging_tone4:': '🤷ðŸ¾â€â™‚ï¸', + ':man_shrugging_dark_skin_tone:': '🤷ðŸ¿â€â™‚ï¸', + ':man_shrugging_tone5:': '🤷ðŸ¿â€â™‚ï¸', + ':man_standing_light_skin_tone:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':man_standing_tone1:': 'ðŸ§ðŸ»â€â™‚ï¸', + ':man_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':man_standing_tone2:': 'ðŸ§ðŸ¼â€â™‚ï¸', + ':man_standing_medium_skin_tone:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':man_standing_tone3:': 'ðŸ§ðŸ½â€â™‚ï¸', + ':man_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':man_standing_tone4:': 'ðŸ§ðŸ¾â€â™‚ï¸', + ':man_standing_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':man_standing_tone5:': 'ðŸ§ðŸ¿â€â™‚ï¸', + ':man_superhero_light_skin_tone:': '🦸ðŸ»â€â™‚ï¸', + ':man_superhero_tone1:': '🦸ðŸ»â€â™‚ï¸', + ':man_superhero_medium_light_skin_tone:': '🦸ðŸ¼â€â™‚ï¸', + ':man_superhero_tone2:': '🦸ðŸ¼â€â™‚ï¸', + ':man_superhero_medium_skin_tone:': '🦸ðŸ½â€â™‚ï¸', + ':man_superhero_tone3:': '🦸ðŸ½â€â™‚ï¸', + ':man_superhero_medium_dark_skin_tone:': '🦸ðŸ¾â€â™‚ï¸', + ':man_superhero_tone4:': '🦸ðŸ¾â€â™‚ï¸', + ':man_superhero_dark_skin_tone:': '🦸ðŸ¿â€â™‚ï¸', + ':man_superhero_tone5:': '🦸ðŸ¿â€â™‚ï¸', + ':man_supervillain_light_skin_tone:': '🦹ðŸ»â€â™‚ï¸', + ':man_supervillain_tone1:': '🦹ðŸ»â€â™‚ï¸', + ':man_supervillain_medium_light_skin_tone:': '🦹ðŸ¼â€â™‚ï¸', + ':man_supervillain_tone2:': '🦹ðŸ¼â€â™‚ï¸', + ':man_supervillain_medium_skin_tone:': '🦹ðŸ½â€â™‚ï¸', + ':man_supervillain_tone3:': '🦹ðŸ½â€â™‚ï¸', + ':man_supervillain_medium_dark_skin_tone:': '🦹ðŸ¾â€â™‚ï¸', + ':man_supervillain_tone4:': '🦹ðŸ¾â€â™‚ï¸', + ':man_supervillain_dark_skin_tone:': '🦹ðŸ¿â€â™‚ï¸', + ':man_supervillain_tone5:': '🦹ðŸ¿â€â™‚ï¸', + ':man_surfing_light_skin_tone:': 'ðŸ„ðŸ»â€â™‚ï¸', + ':man_surfing_tone1:': 'ðŸ„ðŸ»â€â™‚ï¸', + ':man_surfing_medium_light_skin_tone:': 'ðŸ„ðŸ¼â€â™‚ï¸', + ':man_surfing_tone2:': 'ðŸ„ðŸ¼â€â™‚ï¸', + ':man_surfing_medium_skin_tone:': 'ðŸ„ðŸ½â€â™‚ï¸', + ':man_surfing_tone3:': 'ðŸ„ðŸ½â€â™‚ï¸', + ':man_surfing_medium_dark_skin_tone:': 'ðŸ„ðŸ¾â€â™‚ï¸', + ':man_surfing_tone4:': 'ðŸ„ðŸ¾â€â™‚ï¸', + ':man_surfing_dark_skin_tone:': 'ðŸ„ðŸ¿â€â™‚ï¸', + ':man_surfing_tone5:': 'ðŸ„ðŸ¿â€â™‚ï¸', + ':man_swimming_light_skin_tone:': 'ðŸŠðŸ»â€â™‚ï¸', + ':man_swimming_tone1:': 'ðŸŠðŸ»â€â™‚ï¸', + ':man_swimming_medium_light_skin_tone:': 'ðŸŠðŸ¼â€â™‚ï¸', + ':man_swimming_tone2:': 'ðŸŠðŸ¼â€â™‚ï¸', + ':man_swimming_medium_skin_tone:': 'ðŸŠðŸ½â€â™‚ï¸', + ':man_swimming_tone3:': 'ðŸŠðŸ½â€â™‚ï¸', + ':man_swimming_medium_dark_skin_tone:': 'ðŸŠðŸ¾â€â™‚ï¸', + ':man_swimming_tone4:': 'ðŸŠðŸ¾â€â™‚ï¸', + ':man_swimming_dark_skin_tone:': 'ðŸŠðŸ¿â€â™‚ï¸', + ':man_swimming_tone5:': 'ðŸŠðŸ¿â€â™‚ï¸', + ':man_tipping_hand_light_skin_tone:': 'ðŸ’ðŸ»â€â™‚ï¸', + ':man_tipping_hand_tone1:': 'ðŸ’ðŸ»â€â™‚ï¸', + ':man_tipping_hand_medium_light_skin_tone:': 'ðŸ’ðŸ¼â€â™‚ï¸', + ':man_tipping_hand_tone2:': 'ðŸ’ðŸ¼â€â™‚ï¸', + ':man_tipping_hand_medium_skin_tone:': 'ðŸ’ðŸ½â€â™‚ï¸', + ':man_tipping_hand_tone3:': 'ðŸ’ðŸ½â€â™‚ï¸', + ':man_tipping_hand_medium_dark_skin_tone:': 'ðŸ’ðŸ¾â€â™‚ï¸', + ':man_tipping_hand_tone4:': 'ðŸ’ðŸ¾â€â™‚ï¸', + ':man_tipping_hand_dark_skin_tone:': 'ðŸ’ðŸ¿â€â™‚ï¸', + ':man_tipping_hand_tone5:': 'ðŸ’ðŸ¿â€â™‚ï¸', + ':man_vampire_light_skin_tone:': '🧛ðŸ»â€â™‚ï¸', + ':man_vampire_tone1:': '🧛ðŸ»â€â™‚ï¸', + ':man_vampire_medium_light_skin_tone:': '🧛ðŸ¼â€â™‚ï¸', + ':man_vampire_tone2:': '🧛ðŸ¼â€â™‚ï¸', + ':man_vampire_medium_skin_tone:': '🧛ðŸ½â€â™‚ï¸', + ':man_vampire_tone3:': '🧛ðŸ½â€â™‚ï¸', + ':man_vampire_medium_dark_skin_tone:': '🧛ðŸ¾â€â™‚ï¸', + ':man_vampire_tone4:': '🧛ðŸ¾â€â™‚ï¸', + ':man_vampire_dark_skin_tone:': '🧛ðŸ¿â€â™‚ï¸', + ':man_vampire_tone5:': '🧛ðŸ¿â€â™‚ï¸', + ':man_walking_light_skin_tone:': '🚶ðŸ»â€â™‚ï¸', + ':man_walking_tone1:': '🚶ðŸ»â€â™‚ï¸', + ':man_walking_medium_light_skin_tone:': '🚶ðŸ¼â€â™‚ï¸', + ':man_walking_tone2:': '🚶ðŸ¼â€â™‚ï¸', + ':man_walking_medium_skin_tone:': '🚶ðŸ½â€â™‚ï¸', + ':man_walking_tone3:': '🚶ðŸ½â€â™‚ï¸', + ':man_walking_medium_dark_skin_tone:': '🚶ðŸ¾â€â™‚ï¸', + ':man_walking_tone4:': '🚶ðŸ¾â€â™‚ï¸', + ':man_walking_dark_skin_tone:': '🚶ðŸ¿â€â™‚ï¸', + ':man_walking_tone5:': '🚶ðŸ¿â€â™‚ï¸', + ':man_wearing_turban_light_skin_tone:': '👳ðŸ»â€â™‚ï¸', + ':man_wearing_turban_tone1:': '👳ðŸ»â€â™‚ï¸', + ':man_wearing_turban_medium_light_skin_tone:': '👳ðŸ¼â€â™‚ï¸', + ':man_wearing_turban_tone2:': '👳ðŸ¼â€â™‚ï¸', + ':man_wearing_turban_medium_skin_tone:': '👳ðŸ½â€â™‚ï¸', + ':man_wearing_turban_tone3:': '👳ðŸ½â€â™‚ï¸', + ':man_wearing_turban_medium_dark_skin_tone:': '👳ðŸ¾â€â™‚ï¸', + ':man_wearing_turban_tone4:': '👳ðŸ¾â€â™‚ï¸', + ':man_wearing_turban_dark_skin_tone:': '👳ðŸ¿â€â™‚ï¸', + ':man_wearing_turban_tone5:': '👳ðŸ¿â€â™‚ï¸', + ':mermaid_light_skin_tone:': '🧜ðŸ»â€â™€ï¸', + ':mermaid_tone1:': '🧜ðŸ»â€â™€ï¸', + ':mermaid_medium_light_skin_tone:': '🧜ðŸ¼â€â™€ï¸', + ':mermaid_tone2:': '🧜ðŸ¼â€â™€ï¸', + ':mermaid_medium_skin_tone:': '🧜ðŸ½â€â™€ï¸', + ':mermaid_tone3:': '🧜ðŸ½â€â™€ï¸', + ':mermaid_medium_dark_skin_tone:': '🧜ðŸ¾â€â™€ï¸', + ':mermaid_tone4:': '🧜ðŸ¾â€â™€ï¸', + ':mermaid_dark_skin_tone:': '🧜ðŸ¿â€â™€ï¸', + ':mermaid_tone5:': '🧜ðŸ¿â€â™€ï¸', + ':merman_light_skin_tone:': '🧜ðŸ»â€â™‚ï¸', + ':merman_tone1:': '🧜ðŸ»â€â™‚ï¸', + ':merman_medium_light_skin_tone:': '🧜ðŸ¼â€â™‚ï¸', + ':merman_tone2:': '🧜ðŸ¼â€â™‚ï¸', + ':merman_medium_skin_tone:': '🧜ðŸ½â€â™‚ï¸', + ':merman_tone3:': '🧜ðŸ½â€â™‚ï¸', + ':merman_medium_dark_skin_tone:': '🧜ðŸ¾â€â™‚ï¸', + ':merman_tone4:': '🧜ðŸ¾â€â™‚ï¸', + ':merman_dark_skin_tone:': '🧜ðŸ¿â€â™‚ï¸', + ':merman_tone5:': '🧜ðŸ¿â€â™‚ï¸', + ':woman_biking_light_skin_tone:': '🚴ðŸ»â€â™€ï¸', + ':woman_biking_tone1:': '🚴ðŸ»â€â™€ï¸', + ':woman_biking_medium_light_skin_tone:': '🚴ðŸ¼â€â™€ï¸', + ':woman_biking_tone2:': '🚴ðŸ¼â€â™€ï¸', + ':woman_biking_medium_skin_tone:': '🚴ðŸ½â€â™€ï¸', + ':woman_biking_tone3:': '🚴ðŸ½â€â™€ï¸', + ':woman_biking_medium_dark_skin_tone:': '🚴ðŸ¾â€â™€ï¸', + ':woman_biking_tone4:': '🚴ðŸ¾â€â™€ï¸', + ':woman_biking_dark_skin_tone:': '🚴ðŸ¿â€â™€ï¸', + ':woman_biking_tone5:': '🚴ðŸ¿â€â™€ï¸', + ':woman_bowing_light_skin_tone:': '🙇ðŸ»â€â™€ï¸', + ':woman_bowing_tone1:': '🙇ðŸ»â€â™€ï¸', + ':woman_bowing_medium_light_skin_tone:': '🙇ðŸ¼â€â™€ï¸', + ':woman_bowing_tone2:': '🙇ðŸ¼â€â™€ï¸', + ':woman_bowing_medium_skin_tone:': '🙇ðŸ½â€â™€ï¸', + ':woman_bowing_tone3:': '🙇ðŸ½â€â™€ï¸', + ':woman_bowing_medium_dark_skin_tone:': '🙇ðŸ¾â€â™€ï¸', + ':woman_bowing_tone4:': '🙇ðŸ¾â€â™€ï¸', + ':woman_bowing_dark_skin_tone:': '🙇ðŸ¿â€â™€ï¸', + ':woman_bowing_tone5:': '🙇ðŸ¿â€â™€ï¸', + ':woman_cartwheeling_light_skin_tone:': '🤸ðŸ»â€â™€ï¸', + ':woman_cartwheeling_tone1:': '🤸ðŸ»â€â™€ï¸', + ':woman_cartwheeling_medium_light_skin_tone:': '🤸ðŸ¼â€â™€ï¸', + ':woman_cartwheeling_tone2:': '🤸ðŸ¼â€â™€ï¸', + ':woman_cartwheeling_medium_skin_tone:': '🤸ðŸ½â€â™€ï¸', + ':woman_cartwheeling_tone3:': '🤸ðŸ½â€â™€ï¸', + ':woman_cartwheeling_medium_dark_skin_tone:': '🤸ðŸ¾â€â™€ï¸', + ':woman_cartwheeling_tone4:': '🤸ðŸ¾â€â™€ï¸', + ':woman_cartwheeling_dark_skin_tone:': '🤸ðŸ¿â€â™€ï¸', + ':woman_cartwheeling_tone5:': '🤸ðŸ¿â€â™€ï¸', + ':woman_climbing_light_skin_tone:': '🧗ðŸ»â€â™€ï¸', + ':woman_climbing_tone1:': '🧗ðŸ»â€â™€ï¸', + ':woman_climbing_medium_light_skin_tone:': '🧗ðŸ¼â€â™€ï¸', + ':woman_climbing_tone2:': '🧗ðŸ¼â€â™€ï¸', + ':woman_climbing_medium_skin_tone:': '🧗ðŸ½â€â™€ï¸', + ':woman_climbing_tone3:': '🧗ðŸ½â€â™€ï¸', + ':woman_climbing_medium_dark_skin_tone:': '🧗ðŸ¾â€â™€ï¸', + ':woman_climbing_tone4:': '🧗ðŸ¾â€â™€ï¸', + ':woman_climbing_dark_skin_tone:': '🧗ðŸ¿â€â™€ï¸', + ':woman_climbing_tone5:': '🧗ðŸ¿â€â™€ï¸', + ':woman_construction_worker_light_skin_tone:': '👷ðŸ»â€â™€ï¸', + ':woman_construction_worker_tone1:': '👷ðŸ»â€â™€ï¸', + ':woman_construction_worker_medium_light_skin_tone:': '👷ðŸ¼â€â™€ï¸', + ':woman_construction_worker_tone2:': '👷ðŸ¼â€â™€ï¸', + ':woman_construction_worker_medium_skin_tone:': '👷ðŸ½â€â™€ï¸', + ':woman_construction_worker_tone3:': '👷ðŸ½â€â™€ï¸', + ':woman_construction_worker_medium_dark_skin_tone:': '👷ðŸ¾â€â™€ï¸', + ':woman_construction_worker_tone4:': '👷ðŸ¾â€â™€ï¸', + ':woman_construction_worker_dark_skin_tone:': '👷ðŸ¿â€â™€ï¸', + ':woman_construction_worker_tone5:': '👷ðŸ¿â€â™€ï¸', + ':woman_detective_light_skin_tone:': '🕵ï¸ðŸ»â€â™€ï¸', + ':woman_detective_tone1:': '🕵ï¸ðŸ»â€â™€ï¸', + ':woman_detective_medium_light_skin_tone:': '🕵ï¸ðŸ¼â€â™€ï¸', + ':woman_detective_tone2:': '🕵ï¸ðŸ¼â€â™€ï¸', + ':woman_detective_medium_skin_tone:': '🕵ï¸ðŸ½â€â™€ï¸', + ':woman_detective_tone3:': '🕵ï¸ðŸ½â€â™€ï¸', + ':woman_detective_medium_dark_skin_tone:': '🕵ï¸ðŸ¾â€â™€ï¸', + ':woman_detective_tone4:': '🕵ï¸ðŸ¾â€â™€ï¸', + ':woman_detective_dark_skin_tone:': '🕵ï¸ðŸ¿â€â™€ï¸', + ':woman_detective_tone5:': '🕵ï¸ðŸ¿â€â™€ï¸', + ':woman_elf_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', + ':woman_elf_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', + ':woman_elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':woman_elf_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':woman_elf_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', + ':woman_elf_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', + ':woman_elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':woman_elf_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':woman_elf_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':woman_elf_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':woman_facepalming_light_skin_tone:': '🤦ðŸ»â€â™€ï¸', + ':woman_facepalming_tone1:': '🤦ðŸ»â€â™€ï¸', + ':woman_facepalming_medium_light_skin_tone:': '🤦ðŸ¼â€â™€ï¸', + ':woman_facepalming_tone2:': '🤦ðŸ¼â€â™€ï¸', + ':woman_facepalming_medium_skin_tone:': '🤦ðŸ½â€â™€ï¸', + ':woman_facepalming_tone3:': '🤦ðŸ½â€â™€ï¸', + ':woman_facepalming_medium_dark_skin_tone:': '🤦ðŸ¾â€â™€ï¸', + ':woman_facepalming_tone4:': '🤦ðŸ¾â€â™€ï¸', + ':woman_facepalming_dark_skin_tone:': '🤦ðŸ¿â€â™€ï¸', + ':woman_facepalming_tone5:': '🤦ðŸ¿â€â™€ï¸', + ':woman_fairy_light_skin_tone:': '🧚ðŸ»â€â™€ï¸', + ':woman_fairy_tone1:': '🧚ðŸ»â€â™€ï¸', + ':woman_fairy_medium_light_skin_tone:': '🧚ðŸ¼â€â™€ï¸', + ':woman_fairy_tone2:': '🧚ðŸ¼â€â™€ï¸', + ':woman_fairy_medium_skin_tone:': '🧚ðŸ½â€â™€ï¸', + ':woman_fairy_tone3:': '🧚ðŸ½â€â™€ï¸', + ':woman_fairy_medium_dark_skin_tone:': '🧚ðŸ¾â€â™€ï¸', + ':woman_fairy_tone4:': '🧚ðŸ¾â€â™€ï¸', + ':woman_fairy_dark_skin_tone:': '🧚ðŸ¿â€â™€ï¸', + ':woman_fairy_tone5:': '🧚ðŸ¿â€â™€ï¸', + ':woman_frowning_light_skin_tone:': 'ðŸ™ðŸ»â€â™€ï¸', + ':woman_frowning_tone1:': 'ðŸ™ðŸ»â€â™€ï¸', + ':woman_frowning_medium_light_skin_tone:': 'ðŸ™ðŸ¼â€â™€ï¸', + ':woman_frowning_tone2:': 'ðŸ™ðŸ¼â€â™€ï¸', + ':woman_frowning_medium_skin_tone:': 'ðŸ™ðŸ½â€â™€ï¸', + ':woman_frowning_tone3:': 'ðŸ™ðŸ½â€â™€ï¸', + ':woman_frowning_medium_dark_skin_tone:': 'ðŸ™ðŸ¾â€â™€ï¸', + ':woman_frowning_tone4:': 'ðŸ™ðŸ¾â€â™€ï¸', + ':woman_frowning_dark_skin_tone:': 'ðŸ™ðŸ¿â€â™€ï¸', + ':woman_frowning_tone5:': 'ðŸ™ðŸ¿â€â™€ï¸', + ':woman_gesturing_no_light_skin_tone:': '🙅ðŸ»â€â™€ï¸', + ':woman_gesturing_no_tone1:': '🙅ðŸ»â€â™€ï¸', + ':woman_gesturing_no_medium_light_skin_tone:': '🙅ðŸ¼â€â™€ï¸', + ':woman_gesturing_no_tone2:': '🙅ðŸ¼â€â™€ï¸', + ':woman_gesturing_no_medium_skin_tone:': '🙅ðŸ½â€â™€ï¸', + ':woman_gesturing_no_tone3:': '🙅ðŸ½â€â™€ï¸', + ':woman_gesturing_no_medium_dark_skin_tone:': '🙅ðŸ¾â€â™€ï¸', + ':woman_gesturing_no_tone4:': '🙅ðŸ¾â€â™€ï¸', + ':woman_gesturing_no_dark_skin_tone:': '🙅ðŸ¿â€â™€ï¸', + ':woman_gesturing_no_tone5:': '🙅ðŸ¿â€â™€ï¸', + ':woman_gesturing_ok_light_skin_tone:': '🙆ðŸ»â€â™€ï¸', + ':woman_gesturing_ok_tone1:': '🙆ðŸ»â€â™€ï¸', + ':woman_gesturing_ok_medium_light_skin_tone:': '🙆ðŸ¼â€â™€ï¸', + ':woman_gesturing_ok_tone2:': '🙆ðŸ¼â€â™€ï¸', + ':woman_gesturing_ok_medium_skin_tone:': '🙆ðŸ½â€â™€ï¸', + ':woman_gesturing_ok_tone3:': '🙆ðŸ½â€â™€ï¸', + ':woman_gesturing_ok_medium_dark_skin_tone:': '🙆ðŸ¾â€â™€ï¸', + ':woman_gesturing_ok_tone4:': '🙆ðŸ¾â€â™€ï¸', + ':woman_gesturing_ok_dark_skin_tone:': '🙆ðŸ¿â€â™€ï¸', + ':woman_gesturing_ok_tone5:': '🙆ðŸ¿â€â™€ï¸', + ':woman_getting_face_massage_light_skin_tone:': '💆ðŸ»â€â™€ï¸', + ':woman_getting_face_massage_tone1:': '💆ðŸ»â€â™€ï¸', + ':woman_getting_face_massage_medium_light_skin_tone:': '💆ðŸ¼â€â™€ï¸', + ':woman_getting_face_massage_tone2:': '💆ðŸ¼â€â™€ï¸', + ':woman_getting_face_massage_medium_skin_tone:': '💆ðŸ½â€â™€ï¸', + ':woman_getting_face_massage_tone3:': '💆ðŸ½â€â™€ï¸', + ':woman_getting_face_massage_medium_dark_skin_tone:': '💆ðŸ¾â€â™€ï¸', + ':woman_getting_face_massage_tone4:': '💆ðŸ¾â€â™€ï¸', + ':woman_getting_face_massage_dark_skin_tone:': '💆ðŸ¿â€â™€ï¸', + ':woman_getting_face_massage_tone5:': '💆ðŸ¿â€â™€ï¸', + ':woman_getting_haircut_light_skin_tone:': '💇ðŸ»â€â™€ï¸', + ':woman_getting_haircut_tone1:': '💇ðŸ»â€â™€ï¸', + ':woman_getting_haircut_medium_light_skin_tone:': '💇ðŸ¼â€â™€ï¸', + ':woman_getting_haircut_tone2:': '💇ðŸ¼â€â™€ï¸', + ':woman_getting_haircut_medium_skin_tone:': '💇ðŸ½â€â™€ï¸', + ':woman_getting_haircut_tone3:': '💇ðŸ½â€â™€ï¸', + ':woman_getting_haircut_medium_dark_skin_tone:': '💇ðŸ¾â€â™€ï¸', + ':woman_getting_haircut_tone4:': '💇ðŸ¾â€â™€ï¸', + ':woman_getting_haircut_dark_skin_tone:': '💇ðŸ¿â€â™€ï¸', + ':woman_getting_haircut_tone5:': '💇ðŸ¿â€â™€ï¸', + ':woman_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»â€â™€ï¸', + ':woman_golfing_tone1:': 'ðŸŒï¸ðŸ»â€â™€ï¸', + ':woman_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼â€â™€ï¸', + ':woman_golfing_tone2:': 'ðŸŒï¸ðŸ¼â€â™€ï¸', + ':woman_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½â€â™€ï¸', + ':woman_golfing_tone3:': 'ðŸŒï¸ðŸ½â€â™€ï¸', + ':woman_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾â€â™€ï¸', + ':woman_golfing_tone4:': 'ðŸŒï¸ðŸ¾â€â™€ï¸', + ':woman_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿â€â™€ï¸', + ':woman_golfing_tone5:': 'ðŸŒï¸ðŸ¿â€â™€ï¸', + ':woman_guard_light_skin_tone:': '💂ðŸ»â€â™€ï¸', + ':woman_guard_tone1:': '💂ðŸ»â€â™€ï¸', + ':woman_guard_medium_light_skin_tone:': '💂ðŸ¼â€â™€ï¸', + ':woman_guard_tone2:': '💂ðŸ¼â€â™€ï¸', + ':woman_guard_medium_skin_tone:': '💂ðŸ½â€â™€ï¸', + ':woman_guard_tone3:': '💂ðŸ½â€â™€ï¸', + ':woman_guard_medium_dark_skin_tone:': '💂ðŸ¾â€â™€ï¸', + ':woman_guard_tone4:': '💂ðŸ¾â€â™€ï¸', + ':woman_guard_dark_skin_tone:': '💂ðŸ¿â€â™€ï¸', + ':woman_guard_tone5:': '💂ðŸ¿â€â™€ï¸', + ':woman_health_worker_light_skin_tone:': '👩ðŸ»â€âš•ï¸', + ':woman_health_worker_tone1:': '👩ðŸ»â€âš•ï¸', + ':woman_health_worker_medium_light_skin_tone:': '👩ðŸ¼â€âš•ï¸', + ':woman_health_worker_tone2:': '👩ðŸ¼â€âš•ï¸', + ':woman_health_worker_medium_skin_tone:': '👩ðŸ½â€âš•ï¸', + ':woman_health_worker_tone3:': '👩ðŸ½â€âš•ï¸', + ':woman_health_worker_medium_dark_skin_tone:': '👩ðŸ¾â€âš•ï¸', + ':woman_health_worker_tone4:': '👩ðŸ¾â€âš•ï¸', + ':woman_health_worker_dark_skin_tone:': '👩ðŸ¿â€âš•ï¸', + ':woman_health_worker_tone5:': '👩ðŸ¿â€âš•ï¸', + ':woman_in_lotus_position_light_skin_tone:': '🧘ðŸ»â€â™€ï¸', + ':woman_in_lotus_position_tone1:': '🧘ðŸ»â€â™€ï¸', + ':woman_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼â€â™€ï¸', + ':woman_in_lotus_position_tone2:': '🧘ðŸ¼â€â™€ï¸', + ':woman_in_lotus_position_medium_skin_tone:': '🧘ðŸ½â€â™€ï¸', + ':woman_in_lotus_position_tone3:': '🧘ðŸ½â€â™€ï¸', + ':woman_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾â€â™€ï¸', + ':woman_in_lotus_position_tone4:': '🧘ðŸ¾â€â™€ï¸', + ':woman_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿â€â™€ï¸', + ':woman_in_lotus_position_tone5:': '🧘ðŸ¿â€â™€ï¸', + ':woman_in_steamy_room_light_skin_tone:': '🧖ðŸ»â€â™€ï¸', + ':woman_in_steamy_room_tone1:': '🧖ðŸ»â€â™€ï¸', + ':woman_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼â€â™€ï¸', + ':woman_in_steamy_room_tone2:': '🧖ðŸ¼â€â™€ï¸', + ':woman_in_steamy_room_medium_skin_tone:': '🧖ðŸ½â€â™€ï¸', + ':woman_in_steamy_room_tone3:': '🧖ðŸ½â€â™€ï¸', + ':woman_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾â€â™€ï¸', + ':woman_in_steamy_room_tone4:': '🧖ðŸ¾â€â™€ï¸', + ':woman_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿â€â™€ï¸', + ':woman_in_steamy_room_tone5:': '🧖ðŸ¿â€â™€ï¸', + ':woman_judge_light_skin_tone:': '👩ðŸ»â€âš–ï¸', + ':woman_judge_tone1:': '👩ðŸ»â€âš–ï¸', + ':woman_judge_medium_light_skin_tone:': '👩ðŸ¼â€âš–ï¸', + ':woman_judge_tone2:': '👩ðŸ¼â€âš–ï¸', + ':woman_judge_medium_skin_tone:': '👩ðŸ½â€âš–ï¸', + ':woman_judge_tone3:': '👩ðŸ½â€âš–ï¸', + ':woman_judge_medium_dark_skin_tone:': '👩ðŸ¾â€âš–ï¸', + ':woman_judge_tone4:': '👩ðŸ¾â€âš–ï¸', + ':woman_judge_dark_skin_tone:': '👩ðŸ¿â€âš–ï¸', + ':woman_judge_tone5:': '👩ðŸ¿â€âš–ï¸', + ':woman_juggling_light_skin_tone:': '🤹ðŸ»â€â™€ï¸', + ':woman_juggling_tone1:': '🤹ðŸ»â€â™€ï¸', + ':woman_juggling_medium_light_skin_tone:': '🤹ðŸ¼â€â™€ï¸', + ':woman_juggling_tone2:': '🤹ðŸ¼â€â™€ï¸', + ':woman_juggling_medium_skin_tone:': '🤹ðŸ½â€â™€ï¸', + ':woman_juggling_tone3:': '🤹ðŸ½â€â™€ï¸', + ':woman_juggling_medium_dark_skin_tone:': '🤹ðŸ¾â€â™€ï¸', + ':woman_juggling_tone4:': '🤹ðŸ¾â€â™€ï¸', + ':woman_juggling_dark_skin_tone:': '🤹ðŸ¿â€â™€ï¸', + ':woman_juggling_tone5:': '🤹ðŸ¿â€â™€ï¸', + ':woman_kneeling_light_skin_tone:': '🧎ðŸ»â€â™€ï¸', + ':woman_kneeling_tone1:': '🧎ðŸ»â€â™€ï¸', + ':woman_kneeling_medium_light_skin_tone:': '🧎ðŸ¼â€â™€ï¸', + ':woman_kneeling_tone2:': '🧎ðŸ¼â€â™€ï¸', + ':woman_kneeling_medium_skin_tone:': '🧎ðŸ½â€â™€ï¸', + ':woman_kneeling_tone3:': '🧎ðŸ½â€â™€ï¸', + ':woman_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾â€â™€ï¸', + ':woman_kneeling_tone4:': '🧎ðŸ¾â€â™€ï¸', + ':woman_kneeling_dark_skin_tone:': '🧎ðŸ¿â€â™€ï¸', + ':woman_kneeling_tone5:': '🧎ðŸ¿â€â™€ï¸', + ':woman_lifting_weights_light_skin_tone:': 'ðŸ‹ï¸ðŸ»â€â™€ï¸', + ':woman_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»â€â™€ï¸', + ':woman_lifting_weights_medium_light_skin_tone:': 'ðŸ‹ï¸ðŸ¼â€â™€ï¸', + ':woman_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼â€â™€ï¸', + ':woman_lifting_weights_medium_skin_tone:': 'ðŸ‹ï¸ðŸ½â€â™€ï¸', + ':woman_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½â€â™€ï¸', + ':woman_lifting_weights_medium_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¾â€â™€ï¸', + ':woman_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾â€â™€ï¸', + ':woman_lifting_weights_dark_skin_tone:': 'ðŸ‹ï¸ðŸ¿â€â™€ï¸', + ':woman_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿â€â™€ï¸', + ':woman_mage_light_skin_tone:': '🧙ðŸ»â€â™€ï¸', + ':woman_mage_tone1:': '🧙ðŸ»â€â™€ï¸', + ':woman_mage_medium_light_skin_tone:': '🧙ðŸ¼â€â™€ï¸', + ':woman_mage_tone2:': '🧙ðŸ¼â€â™€ï¸', + ':woman_mage_medium_skin_tone:': '🧙ðŸ½â€â™€ï¸', + ':woman_mage_tone3:': '🧙ðŸ½â€â™€ï¸', + ':woman_mage_medium_dark_skin_tone:': '🧙ðŸ¾â€â™€ï¸', + ':woman_mage_tone4:': '🧙ðŸ¾â€â™€ï¸', + ':woman_mage_dark_skin_tone:': '🧙ðŸ¿â€â™€ï¸', + ':woman_mage_tone5:': '🧙ðŸ¿â€â™€ï¸', + ':woman_mountain_biking_light_skin_tone:': '🚵ðŸ»â€â™€ï¸', + ':woman_mountain_biking_tone1:': '🚵ðŸ»â€â™€ï¸', + ':woman_mountain_biking_medium_light_skin_tone:': '🚵ðŸ¼â€â™€ï¸', + ':woman_mountain_biking_tone2:': '🚵ðŸ¼â€â™€ï¸', + ':woman_mountain_biking_medium_skin_tone:': '🚵ðŸ½â€â™€ï¸', + ':woman_mountain_biking_tone3:': '🚵ðŸ½â€â™€ï¸', + ':woman_mountain_biking_medium_dark_skin_tone:': '🚵ðŸ¾â€â™€ï¸', + ':woman_mountain_biking_tone4:': '🚵ðŸ¾â€â™€ï¸', + ':woman_mountain_biking_dark_skin_tone:': '🚵ðŸ¿â€â™€ï¸', + ':woman_mountain_biking_tone5:': '🚵ðŸ¿â€â™€ï¸', + ':woman_pilot_light_skin_tone:': '👩ðŸ»â€âœˆï¸', + ':woman_pilot_tone1:': '👩ðŸ»â€âœˆï¸', + ':woman_pilot_medium_light_skin_tone:': '👩ðŸ¼â€âœˆï¸', + ':woman_pilot_tone2:': '👩ðŸ¼â€âœˆï¸', + ':woman_pilot_medium_skin_tone:': '👩ðŸ½â€âœˆï¸', + ':woman_pilot_tone3:': '👩ðŸ½â€âœˆï¸', + ':woman_pilot_medium_dark_skin_tone:': '👩ðŸ¾â€âœˆï¸', + ':woman_pilot_tone4:': '👩ðŸ¾â€âœˆï¸', + ':woman_pilot_dark_skin_tone:': '👩ðŸ¿â€âœˆï¸', + ':woman_pilot_tone5:': '👩ðŸ¿â€âœˆï¸', + ':woman_playing_handball_light_skin_tone:': '🤾ðŸ»â€â™€ï¸', + ':woman_playing_handball_tone1:': '🤾ðŸ»â€â™€ï¸', + ':woman_playing_handball_medium_light_skin_tone:': '🤾ðŸ¼â€â™€ï¸', + ':woman_playing_handball_tone2:': '🤾ðŸ¼â€â™€ï¸', + ':woman_playing_handball_medium_skin_tone:': '🤾ðŸ½â€â™€ï¸', + ':woman_playing_handball_tone3:': '🤾ðŸ½â€â™€ï¸', + ':woman_playing_handball_medium_dark_skin_tone:': '🤾ðŸ¾â€â™€ï¸', + ':woman_playing_handball_tone4:': '🤾ðŸ¾â€â™€ï¸', + ':woman_playing_handball_dark_skin_tone:': '🤾ðŸ¿â€â™€ï¸', + ':woman_playing_handball_tone5:': '🤾ðŸ¿â€â™€ï¸', + ':woman_playing_water_polo_light_skin_tone:': '🤽ðŸ»â€â™€ï¸', + ':woman_playing_water_polo_tone1:': '🤽ðŸ»â€â™€ï¸', + ':woman_playing_water_polo_medium_light_skin_tone:': '🤽ðŸ¼â€â™€ï¸', + ':woman_playing_water_polo_tone2:': '🤽ðŸ¼â€â™€ï¸', + ':woman_playing_water_polo_medium_skin_tone:': '🤽ðŸ½â€â™€ï¸', + ':woman_playing_water_polo_tone3:': '🤽ðŸ½â€â™€ï¸', + ':woman_playing_water_polo_medium_dark_skin_tone:': '🤽ðŸ¾â€â™€ï¸', + ':woman_playing_water_polo_tone4:': '🤽ðŸ¾â€â™€ï¸', + ':woman_playing_water_polo_dark_skin_tone:': '🤽ðŸ¿â€â™€ï¸', + ':woman_playing_water_polo_tone5:': '🤽ðŸ¿â€â™€ï¸', + ':woman_police_officer_light_skin_tone:': '👮ðŸ»â€â™€ï¸', + ':woman_police_officer_tone1:': '👮ðŸ»â€â™€ï¸', + ':woman_police_officer_medium_light_skin_tone:': '👮ðŸ¼â€â™€ï¸', + ':woman_police_officer_tone2:': '👮ðŸ¼â€â™€ï¸', + ':woman_police_officer_medium_skin_tone:': '👮ðŸ½â€â™€ï¸', + ':woman_police_officer_tone3:': '👮ðŸ½â€â™€ï¸', + ':woman_police_officer_medium_dark_skin_tone:': '👮ðŸ¾â€â™€ï¸', + ':woman_police_officer_tone4:': '👮ðŸ¾â€â™€ï¸', + ':woman_police_officer_dark_skin_tone:': '👮ðŸ¿â€â™€ï¸', + ':woman_police_officer_tone5:': '👮ðŸ¿â€â™€ï¸', + ':woman_pouting_light_skin_tone:': '🙎ðŸ»â€â™€ï¸', + ':woman_pouting_tone1:': '🙎ðŸ»â€â™€ï¸', + ':woman_pouting_medium_light_skin_tone:': '🙎ðŸ¼â€â™€ï¸', + ':woman_pouting_tone2:': '🙎ðŸ¼â€â™€ï¸', + ':woman_pouting_medium_skin_tone:': '🙎ðŸ½â€â™€ï¸', + ':woman_pouting_tone3:': '🙎ðŸ½â€â™€ï¸', + ':woman_pouting_medium_dark_skin_tone:': '🙎ðŸ¾â€â™€ï¸', + ':woman_pouting_tone4:': '🙎ðŸ¾â€â™€ï¸', + ':woman_pouting_dark_skin_tone:': '🙎ðŸ¿â€â™€ï¸', + ':woman_pouting_tone5:': '🙎ðŸ¿â€â™€ï¸', + ':woman_raising_hand_light_skin_tone:': '🙋ðŸ»â€â™€ï¸', + ':woman_raising_hand_tone1:': '🙋ðŸ»â€â™€ï¸', + ':woman_raising_hand_medium_light_skin_tone:': '🙋ðŸ¼â€â™€ï¸', + ':woman_raising_hand_tone2:': '🙋ðŸ¼â€â™€ï¸', + ':woman_raising_hand_medium_skin_tone:': '🙋ðŸ½â€â™€ï¸', + ':woman_raising_hand_tone3:': '🙋ðŸ½â€â™€ï¸', + ':woman_raising_hand_medium_dark_skin_tone:': '🙋ðŸ¾â€â™€ï¸', + ':woman_raising_hand_tone4:': '🙋ðŸ¾â€â™€ï¸', + ':woman_raising_hand_dark_skin_tone:': '🙋ðŸ¿â€â™€ï¸', + ':woman_raising_hand_tone5:': '🙋ðŸ¿â€â™€ï¸', + ':woman_rowing_boat_light_skin_tone:': '🚣ðŸ»â€â™€ï¸', + ':woman_rowing_boat_tone1:': '🚣ðŸ»â€â™€ï¸', + ':woman_rowing_boat_medium_light_skin_tone:': '🚣ðŸ¼â€â™€ï¸', + ':woman_rowing_boat_tone2:': '🚣ðŸ¼â€â™€ï¸', + ':woman_rowing_boat_medium_skin_tone:': '🚣ðŸ½â€â™€ï¸', + ':woman_rowing_boat_tone3:': '🚣ðŸ½â€â™€ï¸', + ':woman_rowing_boat_medium_dark_skin_tone:': '🚣ðŸ¾â€â™€ï¸', + ':woman_rowing_boat_tone4:': '🚣ðŸ¾â€â™€ï¸', + ':woman_rowing_boat_dark_skin_tone:': '🚣ðŸ¿â€â™€ï¸', + ':woman_rowing_boat_tone5:': '🚣ðŸ¿â€â™€ï¸', + ':woman_running_light_skin_tone:': 'ðŸƒðŸ»â€â™€ï¸', + ':woman_running_tone1:': 'ðŸƒðŸ»â€â™€ï¸', + ':woman_running_medium_light_skin_tone:': 'ðŸƒðŸ¼â€â™€ï¸', + ':woman_running_tone2:': 'ðŸƒðŸ¼â€â™€ï¸', + ':woman_running_medium_skin_tone:': 'ðŸƒðŸ½â€â™€ï¸', + ':woman_running_tone3:': 'ðŸƒðŸ½â€â™€ï¸', + ':woman_running_medium_dark_skin_tone:': 'ðŸƒðŸ¾â€â™€ï¸', + ':woman_running_tone4:': 'ðŸƒðŸ¾â€â™€ï¸', + ':woman_running_dark_skin_tone:': 'ðŸƒðŸ¿â€â™€ï¸', + ':woman_running_tone5:': 'ðŸƒðŸ¿â€â™€ï¸', + ':woman_shrugging_light_skin_tone:': '🤷ðŸ»â€â™€ï¸', + ':woman_shrugging_tone1:': '🤷ðŸ»â€â™€ï¸', + ':woman_shrugging_medium_light_skin_tone:': '🤷ðŸ¼â€â™€ï¸', + ':woman_shrugging_tone2:': '🤷ðŸ¼â€â™€ï¸', + ':woman_shrugging_medium_skin_tone:': '🤷ðŸ½â€â™€ï¸', + ':woman_shrugging_tone3:': '🤷ðŸ½â€â™€ï¸', + ':woman_shrugging_medium_dark_skin_tone:': '🤷ðŸ¾â€â™€ï¸', + ':woman_shrugging_tone4:': '🤷ðŸ¾â€â™€ï¸', + ':woman_shrugging_dark_skin_tone:': '🤷ðŸ¿â€â™€ï¸', + ':woman_shrugging_tone5:': '🤷ðŸ¿â€â™€ï¸', + ':woman_standing_light_skin_tone:': 'ðŸ§ðŸ»â€â™€ï¸', + ':woman_standing_tone1:': 'ðŸ§ðŸ»â€â™€ï¸', + ':woman_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':woman_standing_tone2:': 'ðŸ§ðŸ¼â€â™€ï¸', + ':woman_standing_medium_skin_tone:': 'ðŸ§ðŸ½â€â™€ï¸', + ':woman_standing_tone3:': 'ðŸ§ðŸ½â€â™€ï¸', + ':woman_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':woman_standing_tone4:': 'ðŸ§ðŸ¾â€â™€ï¸', + ':woman_standing_dark_skin_tone:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':woman_standing_tone5:': 'ðŸ§ðŸ¿â€â™€ï¸', + ':woman_superhero_light_skin_tone:': '🦸ðŸ»â€â™€ï¸', + ':woman_superhero_tone1:': '🦸ðŸ»â€â™€ï¸', + ':woman_superhero_medium_light_skin_tone:': '🦸ðŸ¼â€â™€ï¸', + ':woman_superhero_tone2:': '🦸ðŸ¼â€â™€ï¸', + ':woman_superhero_medium_skin_tone:': '🦸ðŸ½â€â™€ï¸', + ':woman_superhero_tone3:': '🦸ðŸ½â€â™€ï¸', + ':woman_superhero_medium_dark_skin_tone:': '🦸ðŸ¾â€â™€ï¸', + ':woman_superhero_tone4:': '🦸ðŸ¾â€â™€ï¸', + ':woman_superhero_dark_skin_tone:': '🦸ðŸ¿â€â™€ï¸', + ':woman_superhero_tone5:': '🦸ðŸ¿â€â™€ï¸', + ':woman_supervillain_light_skin_tone:': '🦹ðŸ»â€â™€ï¸', + ':woman_supervillain_tone1:': '🦹ðŸ»â€â™€ï¸', + ':woman_supervillain_medium_light_skin_tone:': '🦹ðŸ¼â€â™€ï¸', + ':woman_supervillain_tone2:': '🦹ðŸ¼â€â™€ï¸', + ':woman_supervillain_medium_skin_tone:': '🦹ðŸ½â€â™€ï¸', + ':woman_supervillain_tone3:': '🦹ðŸ½â€â™€ï¸', + ':woman_supervillain_medium_dark_skin_tone:': '🦹ðŸ¾â€â™€ï¸', + ':woman_supervillain_tone4:': '🦹ðŸ¾â€â™€ï¸', + ':woman_supervillain_dark_skin_tone:': '🦹ðŸ¿â€â™€ï¸', + ':woman_supervillain_tone5:': '🦹ðŸ¿â€â™€ï¸', + ':woman_surfing_light_skin_tone:': 'ðŸ„ðŸ»â€â™€ï¸', + ':woman_surfing_tone1:': 'ðŸ„ðŸ»â€â™€ï¸', + ':woman_surfing_medium_light_skin_tone:': 'ðŸ„ðŸ¼â€â™€ï¸', + ':woman_surfing_tone2:': 'ðŸ„ðŸ¼â€â™€ï¸', + ':woman_surfing_medium_skin_tone:': 'ðŸ„ðŸ½â€â™€ï¸', + ':woman_surfing_tone3:': 'ðŸ„ðŸ½â€â™€ï¸', + ':woman_surfing_medium_dark_skin_tone:': 'ðŸ„ðŸ¾â€â™€ï¸', + ':woman_surfing_tone4:': 'ðŸ„ðŸ¾â€â™€ï¸', + ':woman_surfing_dark_skin_tone:': 'ðŸ„ðŸ¿â€â™€ï¸', + ':woman_surfing_tone5:': 'ðŸ„ðŸ¿â€â™€ï¸', + ':woman_swimming_light_skin_tone:': 'ðŸŠðŸ»â€â™€ï¸', + ':woman_swimming_tone1:': 'ðŸŠðŸ»â€â™€ï¸', + ':woman_swimming_medium_light_skin_tone:': 'ðŸŠðŸ¼â€â™€ï¸', + ':woman_swimming_tone2:': 'ðŸŠðŸ¼â€â™€ï¸', + ':woman_swimming_medium_skin_tone:': 'ðŸŠðŸ½â€â™€ï¸', + ':woman_swimming_tone3:': 'ðŸŠðŸ½â€â™€ï¸', + ':woman_swimming_medium_dark_skin_tone:': 'ðŸŠðŸ¾â€â™€ï¸', + ':woman_swimming_tone4:': 'ðŸŠðŸ¾â€â™€ï¸', + ':woman_swimming_dark_skin_tone:': 'ðŸŠðŸ¿â€â™€ï¸', + ':woman_swimming_tone5:': 'ðŸŠðŸ¿â€â™€ï¸', + ':woman_tipping_hand_light_skin_tone:': 'ðŸ’ðŸ»â€â™€ï¸', + ':woman_tipping_hand_tone1:': 'ðŸ’ðŸ»â€â™€ï¸', + ':woman_tipping_hand_medium_light_skin_tone:': 'ðŸ’ðŸ¼â€â™€ï¸', + ':woman_tipping_hand_tone2:': 'ðŸ’ðŸ¼â€â™€ï¸', + ':woman_tipping_hand_medium_skin_tone:': 'ðŸ’ðŸ½â€â™€ï¸', + ':woman_tipping_hand_tone3:': 'ðŸ’ðŸ½â€â™€ï¸', + ':woman_tipping_hand_medium_dark_skin_tone:': 'ðŸ’ðŸ¾â€â™€ï¸', + ':woman_tipping_hand_tone4:': 'ðŸ’ðŸ¾â€â™€ï¸', + ':woman_tipping_hand_dark_skin_tone:': 'ðŸ’ðŸ¿â€â™€ï¸', + ':woman_tipping_hand_tone5:': 'ðŸ’ðŸ¿â€â™€ï¸', + ':woman_vampire_light_skin_tone:': '🧛ðŸ»â€â™€ï¸', + ':woman_vampire_tone1:': '🧛ðŸ»â€â™€ï¸', + ':woman_vampire_medium_light_skin_tone:': '🧛ðŸ¼â€â™€ï¸', + ':woman_vampire_tone2:': '🧛ðŸ¼â€â™€ï¸', + ':woman_vampire_medium_skin_tone:': '🧛ðŸ½â€â™€ï¸', + ':woman_vampire_tone3:': '🧛ðŸ½â€â™€ï¸', + ':woman_vampire_medium_dark_skin_tone:': '🧛ðŸ¾â€â™€ï¸', + ':woman_vampire_tone4:': '🧛ðŸ¾â€â™€ï¸', + ':woman_vampire_dark_skin_tone:': '🧛ðŸ¿â€â™€ï¸', + ':woman_vampire_tone5:': '🧛ðŸ¿â€â™€ï¸', + ':woman_walking_light_skin_tone:': '🚶ðŸ»â€â™€ï¸', + ':woman_walking_tone1:': '🚶ðŸ»â€â™€ï¸', + ':woman_walking_medium_light_skin_tone:': '🚶ðŸ¼â€â™€ï¸', + ':woman_walking_tone2:': '🚶ðŸ¼â€â™€ï¸', + ':woman_walking_medium_skin_tone:': '🚶ðŸ½â€â™€ï¸', + ':woman_walking_tone3:': '🚶ðŸ½â€â™€ï¸', + ':woman_walking_medium_dark_skin_tone:': '🚶ðŸ¾â€â™€ï¸', + ':woman_walking_tone4:': '🚶ðŸ¾â€â™€ï¸', + ':woman_walking_dark_skin_tone:': '🚶ðŸ¿â€â™€ï¸', + ':woman_walking_tone5:': '🚶ðŸ¿â€â™€ï¸', + ':woman_wearing_turban_light_skin_tone:': '👳ðŸ»â€â™€ï¸', + ':woman_wearing_turban_tone1:': '👳ðŸ»â€â™€ï¸', + ':woman_wearing_turban_medium_light_skin_tone:': '👳ðŸ¼â€â™€ï¸', + ':woman_wearing_turban_tone2:': '👳ðŸ¼â€â™€ï¸', + ':woman_wearing_turban_medium_skin_tone:': '👳ðŸ½â€â™€ï¸', + ':woman_wearing_turban_tone3:': '👳ðŸ½â€â™€ï¸', + ':woman_wearing_turban_medium_dark_skin_tone:': '👳ðŸ¾â€â™€ï¸', + ':woman_wearing_turban_tone4:': '👳ðŸ¾â€â™€ï¸', + ':woman_wearing_turban_dark_skin_tone:': '👳ðŸ¿â€â™€ï¸', + ':woman_wearing_turban_tone5:': '👳ðŸ¿â€â™€ï¸', + ':man_bouncing_ball_light_skin_tone:': '⛹ï¸ðŸ»â€â™‚ï¸', + ':man_bouncing_ball_tone1:': '⛹ï¸ðŸ»â€â™‚ï¸', + ':man_bouncing_ball_medium_light_skin_tone:': '⛹ï¸ðŸ¼â€â™‚ï¸', + ':man_bouncing_ball_tone2:': '⛹ï¸ðŸ¼â€â™‚ï¸', + ':man_bouncing_ball_medium_skin_tone:': '⛹ï¸ðŸ½â€â™‚ï¸', + ':man_bouncing_ball_tone3:': '⛹ï¸ðŸ½â€â™‚ï¸', + ':man_bouncing_ball_medium_dark_skin_tone:': '⛹ï¸ðŸ¾â€â™‚ï¸', + ':man_bouncing_ball_tone4:': '⛹ï¸ðŸ¾â€â™‚ï¸', + ':man_bouncing_ball_dark_skin_tone:': '⛹ï¸ðŸ¿â€â™‚ï¸', + ':man_bouncing_ball_tone5:': '⛹ï¸ðŸ¿â€â™‚ï¸', + ':woman_bouncing_ball_light_skin_tone:': '⛹ï¸ðŸ»â€â™€ï¸', + ':woman_bouncing_ball_tone1:': '⛹ï¸ðŸ»â€â™€ï¸', + ':woman_bouncing_ball_medium_light_skin_tone:': '⛹ï¸ðŸ¼â€â™€ï¸', + ':woman_bouncing_ball_tone2:': '⛹ï¸ðŸ¼â€â™€ï¸', + ':woman_bouncing_ball_medium_skin_tone:': '⛹ï¸ðŸ½â€â™€ï¸', + ':woman_bouncing_ball_tone3:': '⛹ï¸ðŸ½â€â™€ï¸', + ':woman_bouncing_ball_medium_dark_skin_tone:': '⛹ï¸ðŸ¾â€â™€ï¸', + ':woman_bouncing_ball_tone4:': '⛹ï¸ðŸ¾â€â™€ï¸', + ':woman_bouncing_ball_dark_skin_tone:': '⛹ï¸ðŸ¿â€â™€ï¸', + ':woman_bouncing_ball_tone5:': '⛹ï¸ðŸ¿â€â™€ï¸', + ':adult_light_skin_tone:': '🧑ðŸ»', + ':adult_tone1:': '🧑ðŸ»', + ':adult_medium_light_skin_tone:': '🧑ðŸ¼', + ':adult_tone2:': '🧑ðŸ¼', + ':adult_medium_skin_tone:': '🧑ðŸ½', + ':adult_tone3:': '🧑ðŸ½', + ':adult_medium_dark_skin_tone:': '🧑ðŸ¾', + ':adult_tone4:': '🧑ðŸ¾', + ':adult_dark_skin_tone:': '🧑ðŸ¿', + ':adult_tone5:': '🧑ðŸ¿', + ':angel_tone1:': '👼ðŸ»', + ':angel_tone2:': '👼ðŸ¼', + ':angel_tone3:': '👼ðŸ½', + ':angel_tone4:': '👼ðŸ¾', + ':angel_tone5:': '👼ðŸ¿', + ':baby_tone1:': '👶ðŸ»', + ':baby_tone2:': '👶ðŸ¼', + ':baby_tone3:': '👶ðŸ½', + ':baby_tone4:': '👶ðŸ¾', + ':baby_tone5:': '👶ðŸ¿', + ':bath_tone1:': '🛀ðŸ»', + ':bath_tone2:': '🛀ðŸ¼', + ':bath_tone3:': '🛀ðŸ½', + ':bath_tone4:': '🛀ðŸ¾', + ':bath_tone5:': '🛀ðŸ¿', + ':bearded_person_light_skin_tone:': '🧔ðŸ»', + ':bearded_person_tone1:': '🧔ðŸ»', + ':bearded_person_medium_light_skin_tone:': '🧔ðŸ¼', + ':bearded_person_tone2:': '🧔ðŸ¼', + ':bearded_person_medium_skin_tone:': '🧔ðŸ½', + ':bearded_person_tone3:': '🧔ðŸ½', + ':bearded_person_medium_dark_skin_tone:': '🧔ðŸ¾', + ':bearded_person_tone4:': '🧔ðŸ¾', + ':bearded_person_dark_skin_tone:': '🧔ðŸ¿', + ':bearded_person_tone5:': '🧔ðŸ¿', + ':person_with_blond_hair_tone1:': '👱ðŸ»', + ':blond_haired_person_tone1:': '👱ðŸ»', + ':person_with_blond_hair_tone2:': '👱ðŸ¼', + ':blond_haired_person_tone2:': '👱ðŸ¼', + ':person_with_blond_hair_tone3:': '👱ðŸ½', + ':blond_haired_person_tone3:': '👱ðŸ½', + ':person_with_blond_hair_tone4:': '👱ðŸ¾', + ':blond_haired_person_tone4:': '👱ðŸ¾', + ':person_with_blond_hair_tone5:': '👱ðŸ¿', + ':blond_haired_person_tone5:': '👱ðŸ¿', + ':boy_tone1:': '👦ðŸ»', + ':boy_tone2:': '👦ðŸ¼', + ':boy_tone3:': '👦ðŸ½', + ':boy_tone4:': '👦ðŸ¾', + ':boy_tone5:': '👦ðŸ¿', + ':breast_feeding_light_skin_tone:': '🤱ðŸ»', + ':breast_feeding_tone1:': '🤱ðŸ»', + ':breast_feeding_medium_light_skin_tone:': '🤱ðŸ¼', + ':breast_feeding_tone2:': '🤱ðŸ¼', + ':breast_feeding_medium_skin_tone:': '🤱ðŸ½', + ':breast_feeding_tone3:': '🤱ðŸ½', + ':breast_feeding_medium_dark_skin_tone:': '🤱ðŸ¾', + ':breast_feeding_tone4:': '🤱ðŸ¾', + ':breast_feeding_dark_skin_tone:': '🤱ðŸ¿', + ':breast_feeding_tone5:': '🤱ðŸ¿', + ':bride_with_veil_tone1:': '👰ðŸ»', + ':bride_with_veil_tone2:': '👰ðŸ¼', + ':bride_with_veil_tone3:': '👰ðŸ½', + ':bride_with_veil_tone4:': '👰ðŸ¾', + ':bride_with_veil_tone5:': '👰ðŸ¿', + ':call_me_hand_tone1:': '🤙ðŸ»', + ':call_me_tone1:': '🤙ðŸ»', + ':call_me_hand_tone2:': '🤙ðŸ¼', + ':call_me_tone2:': '🤙ðŸ¼', + ':call_me_hand_tone3:': '🤙ðŸ½', + ':call_me_tone3:': '🤙ðŸ½', + ':call_me_hand_tone4:': '🤙ðŸ¾', + ':call_me_tone4:': '🤙ðŸ¾', + ':call_me_hand_tone5:': '🤙ðŸ¿', + ':call_me_tone5:': '🤙ðŸ¿', + ':child_light_skin_tone:': '🧒ðŸ»', + ':child_tone1:': '🧒ðŸ»', + ':child_medium_light_skin_tone:': '🧒ðŸ¼', + ':child_tone2:': '🧒ðŸ¼', + ':child_medium_skin_tone:': '🧒ðŸ½', + ':child_tone3:': '🧒ðŸ½', + ':child_medium_dark_skin_tone:': '🧒ðŸ¾', + ':child_tone4:': '🧒ðŸ¾', + ':child_dark_skin_tone:': '🧒ðŸ¿', + ':child_tone5:': '🧒ðŸ¿', + ':clap_tone1:': 'ðŸ‘ðŸ»', + ':clap_tone2:': 'ðŸ‘ðŸ¼', + ':clap_tone3:': 'ðŸ‘ðŸ½', + ':clap_tone4:': 'ðŸ‘ðŸ¾', + ':clap_tone5:': 'ðŸ‘ðŸ¿', + ':construction_worker_tone1:': '👷ðŸ»', + ':construction_worker_tone2:': '👷ðŸ¼', + ':construction_worker_tone3:': '👷ðŸ½', + ':construction_worker_tone4:': '👷ðŸ¾', + ':construction_worker_tone5:': '👷ðŸ¿', + ':dancer_tone1:': '💃ðŸ»', + ':dancer_tone2:': '💃ðŸ¼', + ':dancer_tone3:': '💃ðŸ½', + ':dancer_tone4:': '💃ðŸ¾', + ':dancer_tone5:': '💃ðŸ¿', + ':deaf_person_light_skin_tone:': 'ðŸ§ðŸ»', + ':deaf_person_tone1:': 'ðŸ§ðŸ»', + ':deaf_person_medium_light_skin_tone:': 'ðŸ§ðŸ¼', + ':deaf_person_tone2:': 'ðŸ§ðŸ¼', + ':deaf_person_medium_skin_tone:': 'ðŸ§ðŸ½', + ':deaf_person_tone3:': 'ðŸ§ðŸ½', + ':deaf_person_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', + ':deaf_person_tone4:': 'ðŸ§ðŸ¾', + ':deaf_person_dark_skin_tone:': 'ðŸ§ðŸ¿', + ':deaf_person_tone5:': 'ðŸ§ðŸ¿', + ':spy_tone1:': '🕵ï¸ðŸ»', + ':sleuth_or_spy_tone1:': '🕵ï¸ðŸ»', + ':detective_tone1:': '🕵ï¸ðŸ»', + ':spy_tone2:': '🕵ï¸ðŸ¼', + ':sleuth_or_spy_tone2:': '🕵ï¸ðŸ¼', + ':detective_tone2:': '🕵ï¸ðŸ¼', + ':spy_tone3:': '🕵ï¸ðŸ½', + ':sleuth_or_spy_tone3:': '🕵ï¸ðŸ½', + ':detective_tone3:': '🕵ï¸ðŸ½', + ':spy_tone4:': '🕵ï¸ðŸ¾', + ':sleuth_or_spy_tone4:': '🕵ï¸ðŸ¾', + ':detective_tone4:': '🕵ï¸ðŸ¾', + ':spy_tone5:': '🕵ï¸ðŸ¿', + ':sleuth_or_spy_tone5:': '🕵ï¸ðŸ¿', + ':detective_tone5:': '🕵ï¸ðŸ¿', + ':ear_tone1:': '👂ðŸ»', + ':ear_tone2:': '👂ðŸ¼', + ':ear_tone3:': '👂ðŸ½', + ':ear_tone4:': '👂ðŸ¾', + ':ear_tone5:': '👂ðŸ¿', + ':ear_with_hearing_aid_light_skin_tone:': '🦻ðŸ»', + ':ear_with_hearing_aid_tone1:': '🦻ðŸ»', + ':ear_with_hearing_aid_medium_light_skin_tone:': '🦻ðŸ¼', + ':ear_with_hearing_aid_tone2:': '🦻ðŸ¼', + ':ear_with_hearing_aid_medium_skin_tone:': '🦻ðŸ½', + ':ear_with_hearing_aid_tone3:': '🦻ðŸ½', + ':ear_with_hearing_aid_medium_dark_skin_tone:': '🦻ðŸ¾', + ':ear_with_hearing_aid_tone4:': '🦻ðŸ¾', + ':ear_with_hearing_aid_dark_skin_tone:': '🦻ðŸ¿', + ':ear_with_hearing_aid_tone5:': '🦻ðŸ¿', + ':elf_light_skin_tone:': 'ðŸ§ðŸ»', + ':elf_tone1:': 'ðŸ§ðŸ»', + ':elf_medium_light_skin_tone:': 'ðŸ§ðŸ¼', + ':elf_tone2:': 'ðŸ§ðŸ¼', + ':elf_medium_skin_tone:': 'ðŸ§ðŸ½', + ':elf_tone3:': 'ðŸ§ðŸ½', + ':elf_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', + ':elf_tone4:': 'ðŸ§ðŸ¾', + ':elf_dark_skin_tone:': 'ðŸ§ðŸ¿', + ':elf_tone5:': 'ðŸ§ðŸ¿', + ':eye_in_speech_bubble:': 'ðŸ‘ï¸â€ðŸ—¨ï¸', + ':fairy_light_skin_tone:': '🧚ðŸ»', + ':fairy_tone1:': '🧚ðŸ»', + ':fairy_medium_light_skin_tone:': '🧚ðŸ¼', + ':fairy_tone2:': '🧚ðŸ¼', + ':fairy_medium_skin_tone:': '🧚ðŸ½', + ':fairy_tone3:': '🧚ðŸ½', + ':fairy_medium_dark_skin_tone:': '🧚ðŸ¾', + ':fairy_tone4:': '🧚ðŸ¾', + ':fairy_dark_skin_tone:': '🧚ðŸ¿', + ':fairy_tone5:': '🧚ðŸ¿', + ':family_man_boy:': '👨â€ðŸ‘¦', + ':family_man_girl:': '👨â€ðŸ‘§', + ':family_woman_boy:': '👩â€ðŸ‘¦', + ':family_woman_girl:': '👩â€ðŸ‘§', + ':hand_with_index_and_middle_fingers_crossed_tone1:': '🤞ðŸ»', + ':fingers_crossed_tone1:': '🤞ðŸ»', + ':hand_with_index_and_middle_fingers_crossed_tone2:': '🤞ðŸ¼', + ':fingers_crossed_tone2:': '🤞ðŸ¼', + ':hand_with_index_and_middle_fingers_crossed_tone3:': '🤞ðŸ½', + ':fingers_crossed_tone3:': '🤞ðŸ½', + ':hand_with_index_and_middle_fingers_crossed_tone4:': '🤞ðŸ¾', + ':fingers_crossed_tone4:': '🤞ðŸ¾', + ':hand_with_index_and_middle_fingers_crossed_tone5:': '🤞ðŸ¿', + ':fingers_crossed_tone5:': '🤞ðŸ¿', + ':ac:': '🇦🇨', + ':flag_ac:': '🇦🇨', + ':ad:': '🇦🇩', + ':flag_ad:': '🇦🇩', + ':ae:': '🇦🇪', + ':flag_ae:': '🇦🇪', + ':af:': '🇦🇫', + ':flag_af:': '🇦🇫', + ':ag:': '🇦🇬', + ':flag_ag:': '🇦🇬', + ':ai:': '🇦🇮', + ':flag_ai:': '🇦🇮', + ':al:': '🇦🇱', + ':flag_al:': '🇦🇱', + ':am:': '🇦🇲', + ':flag_am:': '🇦🇲', + ':ao:': '🇦🇴', + ':flag_ao:': '🇦🇴', + ':aq:': '🇦🇶', + ':flag_aq:': '🇦🇶', + ':ar:': '🇦🇷', + ':flag_ar:': '🇦🇷', + ':as:': '🇦🇸', + ':flag_as:': '🇦🇸', + ':at:': '🇦🇹', + ':flag_at:': '🇦🇹', + ':au:': '🇦🇺', + ':flag_au:': '🇦🇺', + ':aw:': '🇦🇼', + ':flag_aw:': '🇦🇼', + ':ax:': '🇦🇽', + ':flag_ax:': '🇦🇽', + ':az:': '🇦🇿', + ':flag_az:': '🇦🇿', + ':ba:': '🇧🇦', + ':flag_ba:': '🇧🇦', + ':bb:': '🇧🇧', + ':flag_bb:': '🇧🇧', + ':bd:': '🇧🇩', + ':flag_bd:': '🇧🇩', + ':be:': '🇧🇪', + ':flag_be:': '🇧🇪', + ':bf:': '🇧🇫', + ':flag_bf:': '🇧🇫', + ':bg:': '🇧🇬', + ':flag_bg:': '🇧🇬', + ':bh:': '🇧ðŸ‡', + ':flag_bh:': '🇧ðŸ‡', + ':bi:': '🇧🇮', + ':flag_bi:': '🇧🇮', + ':bj:': '🇧🇯', + ':flag_bj:': '🇧🇯', + ':bm:': '🇧🇲', + ':flag_bm:': '🇧🇲', + ':bn:': '🇧🇳', + ':flag_bn:': '🇧🇳', + ':bo:': '🇧🇴', + ':flag_bo:': '🇧🇴', + ':br:': '🇧🇷', + ':flag_br:': '🇧🇷', + ':bs:': '🇧🇸', + ':flag_bs:': '🇧🇸', + ':bt:': '🇧🇹', + ':flag_bt:': '🇧🇹', + ':bv:': '🇧🇻', + ':flag_bv:': '🇧🇻', + ':bw:': '🇧🇼', + ':flag_bw:': '🇧🇼', + ':by:': '🇧🇾', + ':flag_by:': '🇧🇾', + ':bz:': '🇧🇿', + ':flag_bz:': '🇧🇿', + ':ca:': '🇨🇦', + ':flag_ca:': '🇨🇦', + ':cc:': '🇨🇨', + ':flag_cc:': '🇨🇨', + ':congo:': '🇨🇩', + ':flag_cd:': '🇨🇩', + ':cf:': '🇨🇫', + ':flag_cf:': '🇨🇫', + ':cg:': '🇨🇬', + ':flag_cg:': '🇨🇬', + ':ch:': '🇨ðŸ‡', + ':flag_ch:': '🇨ðŸ‡', + ':ci:': '🇨🇮', + ':flag_ci:': '🇨🇮', + ':ck:': '🇨🇰', + ':flag_ck:': '🇨🇰', + ':chile:': '🇨🇱', + ':flag_cl:': '🇨🇱', + ':cm:': '🇨🇲', + ':flag_cm:': '🇨🇲', + ':cn:': '🇨🇳', + ':flag_cn:': '🇨🇳', + ':co:': '🇨🇴', + ':flag_co:': '🇨🇴', + ':cp:': '🇨🇵', + ':flag_cp:': '🇨🇵', + ':cr:': '🇨🇷', + ':flag_cr:': '🇨🇷', + ':cu:': '🇨🇺', + ':flag_cu:': '🇨🇺', + ':cv:': '🇨🇻', + ':flag_cv:': '🇨🇻', + ':cw:': '🇨🇼', + ':flag_cw:': '🇨🇼', + ':cx:': '🇨🇽', + ':flag_cx:': '🇨🇽', + ':cy:': '🇨🇾', + ':flag_cy:': '🇨🇾', + ':cz:': '🇨🇿', + ':flag_cz:': '🇨🇿', + ':de:': '🇩🇪', + ':flag_de:': '🇩🇪', + ':dj:': '🇩🇯', + ':flag_dj:': '🇩🇯', + ':dk:': '🇩🇰', + ':flag_dk:': '🇩🇰', + ':dm:': '🇩🇲', + ':flag_dm:': '🇩🇲', + ':do:': '🇩🇴', + ':flag_do:': '🇩🇴', + ':dz:': '🇩🇿', + ':flag_dz:': '🇩🇿', + ':ec:': '🇪🇨', + ':flag_ec:': '🇪🇨', + ':ee:': '🇪🇪', + ':flag_ee:': '🇪🇪', + ':eg:': '🇪🇬', + ':flag_eg:': '🇪🇬', + ':er:': '🇪🇷', + ':flag_er:': '🇪🇷', + ':es:': '🇪🇸', + ':flag_es:': '🇪🇸', + ':et:': '🇪🇹', + ':flag_et:': '🇪🇹', + ':eu:': '🇪🇺', + ':flag_eu:': '🇪🇺', + ':fi:': '🇫🇮', + ':flag_fi:': '🇫🇮', + ':fj:': '🇫🇯', + ':flag_fj:': '🇫🇯', + ':fm:': '🇫🇲', + ':flag_fm:': '🇫🇲', + ':fo:': '🇫🇴', + ':flag_fo:': '🇫🇴', + ':fr:': '🇫🇷', + ':flag_fr:': '🇫🇷', + ':ga:': '🇬🇦', + ':flag_ga:': '🇬🇦', + ':gb:': '🇬🇧', + ':flag_gb:': '🇬🇧', + ':gd:': '🇬🇩', + ':flag_gd:': '🇬🇩', + ':ge:': '🇬🇪', + ':flag_ge:': '🇬🇪', + ':gg:': '🇬🇬', + ':flag_gg:': '🇬🇬', + ':gh:': '🇬ðŸ‡', + ':flag_gh:': '🇬ðŸ‡', + ':gi:': '🇬🇮', + ':flag_gi:': '🇬🇮', + ':gl:': '🇬🇱', + ':flag_gl:': '🇬🇱', + ':gm:': '🇬🇲', + ':flag_gm:': '🇬🇲', + ':gn:': '🇬🇳', + ':flag_gn:': '🇬🇳', + ':gq:': '🇬🇶', + ':flag_gq:': '🇬🇶', + ':gr:': '🇬🇷', + ':flag_gr:': '🇬🇷', + ':gt:': '🇬🇹', + ':flag_gt:': '🇬🇹', + ':gu:': '🇬🇺', + ':flag_gu:': '🇬🇺', + ':gw:': '🇬🇼', + ':flag_gw:': '🇬🇼', + ':gy:': '🇬🇾', + ':flag_gy:': '🇬🇾', + ':hk:': 'ðŸ‡ðŸ‡°', + ':flag_hk:': 'ðŸ‡ðŸ‡°', + ':hm:': 'ðŸ‡ðŸ‡²', + ':flag_hm:': 'ðŸ‡ðŸ‡²', + ':hn:': 'ðŸ‡ðŸ‡³', + ':flag_hn:': 'ðŸ‡ðŸ‡³', + ':hr:': 'ðŸ‡ðŸ‡·', + ':flag_hr:': 'ðŸ‡ðŸ‡·', + ':ht:': 'ðŸ‡ðŸ‡¹', + ':flag_ht:': 'ðŸ‡ðŸ‡¹', + ':hu:': 'ðŸ‡ðŸ‡º', + ':flag_hu:': 'ðŸ‡ðŸ‡º', + ':ic:': '🇮🇨', + ':flag_ic:': '🇮🇨', + ':indonesia:': '🇮🇩', + ':flag_id:': '🇮🇩', + ':ie:': '🇮🇪', + ':flag_ie:': '🇮🇪', + ':il:': '🇮🇱', + ':flag_il:': '🇮🇱', + ':im:': '🇮🇲', + ':flag_im:': '🇮🇲', + ':in:': '🇮🇳', + ':flag_in:': '🇮🇳', + ':io:': '🇮🇴', + ':flag_io:': '🇮🇴', + ':iq:': '🇮🇶', + ':flag_iq:': '🇮🇶', + ':ir:': '🇮🇷', + ':flag_ir:': '🇮🇷', + ':is:': '🇮🇸', + ':flag_is:': '🇮🇸', + ':it:': '🇮🇹', + ':flag_it:': '🇮🇹', + ':je:': '🇯🇪', + ':flag_je:': '🇯🇪', + ':jm:': '🇯🇲', + ':flag_jm:': '🇯🇲', + ':jo:': '🇯🇴', + ':flag_jo:': '🇯🇴', + ':jp:': '🇯🇵', + ':flag_jp:': '🇯🇵', + ':ke:': '🇰🇪', + ':flag_ke:': '🇰🇪', + ':kg:': '🇰🇬', + ':flag_kg:': '🇰🇬', + ':kh:': '🇰ðŸ‡', + ':flag_kh:': '🇰ðŸ‡', + ':ki:': '🇰🇮', + ':flag_ki:': '🇰🇮', + ':km:': '🇰🇲', + ':flag_km:': '🇰🇲', + ':kn:': '🇰🇳', + ':flag_kn:': '🇰🇳', + ':kp:': '🇰🇵', + ':flag_kp:': '🇰🇵', + ':kr:': '🇰🇷', + ':flag_kr:': '🇰🇷', + ':kw:': '🇰🇼', + ':flag_kw:': '🇰🇼', + ':ky:': '🇰🇾', + ':flag_ky:': '🇰🇾', + ':kz:': '🇰🇿', + ':flag_kz:': '🇰🇿', + ':la:': '🇱🇦', + ':flag_la:': '🇱🇦', + ':lb:': '🇱🇧', + ':flag_lb:': '🇱🇧', + ':lc:': '🇱🇨', + ':flag_lc:': '🇱🇨', + ':li:': '🇱🇮', + ':flag_li:': '🇱🇮', + ':lk:': '🇱🇰', + ':flag_lk:': '🇱🇰', + ':lr:': '🇱🇷', + ':flag_lr:': '🇱🇷', + ':ls:': '🇱🇸', + ':flag_ls:': '🇱🇸', + ':lt:': '🇱🇹', + ':flag_lt:': '🇱🇹', + ':lu:': '🇱🇺', + ':flag_lu:': '🇱🇺', + ':lv:': '🇱🇻', + ':flag_lv:': '🇱🇻', + ':ly:': '🇱🇾', + ':flag_ly:': '🇱🇾', + ':ma:': '🇲🇦', + ':flag_ma:': '🇲🇦', + ':mc:': '🇲🇨', + ':flag_mc:': '🇲🇨', + ':md:': '🇲🇩', + ':flag_md:': '🇲🇩', + ':me:': '🇲🇪', + ':flag_me:': '🇲🇪', + ':mg:': '🇲🇬', + ':flag_mg:': '🇲🇬', + ':mh:': '🇲ðŸ‡', + ':flag_mh:': '🇲ðŸ‡', + ':mk:': '🇲🇰', + ':flag_mk:': '🇲🇰', + ':ml:': '🇲🇱', + ':flag_ml:': '🇲🇱', + ':mm:': '🇲🇲', + ':flag_mm:': '🇲🇲', + ':mn:': '🇲🇳', + ':flag_mn:': '🇲🇳', + ':mo:': '🇲🇴', + ':flag_mo:': '🇲🇴', + ':mp:': '🇲🇵', + ':flag_mp:': '🇲🇵', + ':mr:': '🇲🇷', + ':flag_mr:': '🇲🇷', + ':ms:': '🇲🇸', + ':flag_ms:': '🇲🇸', + ':mt:': '🇲🇹', + ':flag_mt:': '🇲🇹', + ':mu:': '🇲🇺', + ':flag_mu:': '🇲🇺', + ':mv:': '🇲🇻', + ':flag_mv:': '🇲🇻', + ':mw:': '🇲🇼', + ':flag_mw:': '🇲🇼', + ':mx:': '🇲🇽', + ':flag_mx:': '🇲🇽', + ':my:': '🇲🇾', + ':flag_my:': '🇲🇾', + ':mz:': '🇲🇿', + ':flag_mz:': '🇲🇿', + ':na:': '🇳🇦', + ':flag_na:': '🇳🇦', + ':ne:': '🇳🇪', + ':flag_ne:': '🇳🇪', + ':nf:': '🇳🇫', + ':flag_nf:': '🇳🇫', + ':nigeria:': '🇳🇬', + ':flag_ng:': '🇳🇬', + ':ni:': '🇳🇮', + ':flag_ni:': '🇳🇮', + ':nl:': '🇳🇱', + ':flag_nl:': '🇳🇱', + ':no:': '🇳🇴', + ':flag_no:': '🇳🇴', + ':np:': '🇳🇵', + ':flag_np:': '🇳🇵', + ':nr:': '🇳🇷', + ':flag_nr:': '🇳🇷', + ':nu:': '🇳🇺', + ':flag_nu:': '🇳🇺', + ':nz:': '🇳🇿', + ':flag_nz:': '🇳🇿', + ':om:': '🇴🇲', + ':flag_om:': '🇴🇲', + ':pa:': '🇵🇦', + ':flag_pa:': '🇵🇦', + ':pe:': '🇵🇪', + ':flag_pe:': '🇵🇪', + ':pf:': '🇵🇫', + ':flag_pf:': '🇵🇫', + ':pg:': '🇵🇬', + ':flag_pg:': '🇵🇬', + ':ph:': '🇵ðŸ‡', + ':flag_ph:': '🇵ðŸ‡', + ':pk:': '🇵🇰', + ':flag_pk:': '🇵🇰', + ':pl:': '🇵🇱', + ':flag_pl:': '🇵🇱', + ':pn:': '🇵🇳', + ':flag_pn:': '🇵🇳', + ':pr:': '🇵🇷', + ':flag_pr:': '🇵🇷', + ':ps:': '🇵🇸', + ':flag_ps:': '🇵🇸', + ':pt:': '🇵🇹', + ':flag_pt:': '🇵🇹', + ':pw:': '🇵🇼', + ':flag_pw:': '🇵🇼', + ':py:': '🇵🇾', + ':flag_py:': '🇵🇾', + ':qa:': '🇶🇦', + ':flag_qa:': '🇶🇦', + ':ro:': '🇷🇴', + ':flag_ro:': '🇷🇴', + ':rs:': '🇷🇸', + ':flag_rs:': '🇷🇸', + ':ru:': '🇷🇺', + ':flag_ru:': '🇷🇺', + ':rw:': '🇷🇼', + ':flag_rw:': '🇷🇼', + ':saudiarabia:': '🇸🇦', + ':saudi:': '🇸🇦', + ':flag_sa:': '🇸🇦', + ':sb:': '🇸🇧', + ':flag_sb:': '🇸🇧', + ':sc:': '🇸🇨', + ':flag_sc:': '🇸🇨', + ':sd:': '🇸🇩', + ':flag_sd:': '🇸🇩', + ':se:': '🇸🇪', + ':flag_se:': '🇸🇪', + ':sg:': '🇸🇬', + ':flag_sg:': '🇸🇬', + ':sh:': '🇸ðŸ‡', + ':flag_sh:': '🇸ðŸ‡', + ':si:': '🇸🇮', + ':flag_si:': '🇸🇮', + ':sj:': '🇸🇯', + ':flag_sj:': '🇸🇯', + ':sk:': '🇸🇰', + ':flag_sk:': '🇸🇰', + ':sl:': '🇸🇱', + ':flag_sl:': '🇸🇱', + ':sm:': '🇸🇲', + ':flag_sm:': '🇸🇲', + ':sn:': '🇸🇳', + ':flag_sn:': '🇸🇳', + ':so:': '🇸🇴', + ':flag_so:': '🇸🇴', + ':sr:': '🇸🇷', + ':flag_sr:': '🇸🇷', + ':ss:': '🇸🇸', + ':flag_ss:': '🇸🇸', + ':st:': '🇸🇹', + ':flag_st:': '🇸🇹', + ':sv:': '🇸🇻', + ':flag_sv:': '🇸🇻', + ':sx:': '🇸🇽', + ':flag_sx:': '🇸🇽', + ':sy:': '🇸🇾', + ':flag_sy:': '🇸🇾', + ':sz:': '🇸🇿', + ':flag_sz:': '🇸🇿', + ':ta:': '🇹🇦', + ':flag_ta:': '🇹🇦', + ':tc:': '🇹🇨', + ':flag_tc:': '🇹🇨', + ':td:': '🇹🇩', + ':flag_td:': '🇹🇩', + ':tg:': '🇹🇬', + ':flag_tg:': '🇹🇬', + ':th:': '🇹ðŸ‡', + ':flag_th:': '🇹ðŸ‡', + ':tj:': '🇹🇯', + ':flag_tj:': '🇹🇯', + ':tk:': '🇹🇰', + ':flag_tk:': '🇹🇰', + ':tl:': '🇹🇱', + ':flag_tl:': '🇹🇱', + ':turkmenistan:': '🇹🇲', + ':flag_tm:': '🇹🇲', + ':tn:': '🇹🇳', + ':flag_tn:': '🇹🇳', + ':to:': '🇹🇴', + ':flag_to:': '🇹🇴', + ':tr:': '🇹🇷', + ':flag_tr:': '🇹🇷', + ':tt:': '🇹🇹', + ':flag_tt:': '🇹🇹', + ':tuvalu:': '🇹🇻', + ':flag_tv:': '🇹🇻', + ':tw:': '🇹🇼', + ':flag_tw:': '🇹🇼', + ':tz:': '🇹🇿', + ':flag_tz:': '🇹🇿', + ':ua:': '🇺🇦', + ':flag_ua:': '🇺🇦', + ':ug:': '🇺🇬', + ':flag_ug:': '🇺🇬', + ':um:': '🇺🇲', + ':flag_um:': '🇺🇲', + ':us:': '🇺🇸', + ':flag_us:': '🇺🇸', + ':uy:': '🇺🇾', + ':flag_uy:': '🇺🇾', + ':uz:': '🇺🇿', + ':flag_uz:': '🇺🇿', + ':va:': '🇻🇦', + ':flag_va:': '🇻🇦', + ':vc:': '🇻🇨', + ':flag_vc:': '🇻🇨', + ':ve:': '🇻🇪', + ':flag_ve:': '🇻🇪', + ':vg:': '🇻🇬', + ':flag_vg:': '🇻🇬', + ':vi:': '🇻🇮', + ':flag_vi:': '🇻🇮', + ':vn:': '🇻🇳', + ':flag_vn:': '🇻🇳', + ':vu:': '🇻🇺', + ':flag_vu:': '🇻🇺', + ':ws:': '🇼🇸', + ':flag_ws:': '🇼🇸', + ':ye:': '🇾🇪', + ':flag_ye:': '🇾🇪', + ':za:': '🇿🇦', + ':flag_za:': '🇿🇦', + ':zm:': '🇿🇲', + ':flag_zm:': '🇿🇲', + ':zw:': '🇿🇼', + ':flag_zw:': '🇿🇼', + ':foot_light_skin_tone:': '🦶ðŸ»', + ':foot_tone1:': '🦶ðŸ»', + ':foot_medium_light_skin_tone:': '🦶ðŸ¼', + ':foot_tone2:': '🦶ðŸ¼', + ':foot_medium_skin_tone:': '🦶ðŸ½', + ':foot_tone3:': '🦶ðŸ½', + ':foot_medium_dark_skin_tone:': '🦶ðŸ¾', + ':foot_tone4:': '🦶ðŸ¾', + ':foot_dark_skin_tone:': '🦶ðŸ¿', + ':foot_tone5:': '🦶ðŸ¿', + ':girl_tone1:': '👧ðŸ»', + ':girl_tone2:': '👧ðŸ¼', + ':girl_tone3:': '👧ðŸ½', + ':girl_tone4:': '👧ðŸ¾', + ':girl_tone5:': '👧ðŸ¿', + ':guardsman_tone1:': '💂ðŸ»', + ':guard_tone1:': '💂ðŸ»', + ':guardsman_tone2:': '💂ðŸ¼', + ':guard_tone2:': '💂ðŸ¼', + ':guardsman_tone3:': '💂ðŸ½', + ':guard_tone3:': '💂ðŸ½', + ':guardsman_tone4:': '💂ðŸ¾', + ':guard_tone4:': '💂ðŸ¾', + ':guardsman_tone5:': '💂ðŸ¿', + ':guard_tone5:': '💂ðŸ¿', + ':raised_hand_with_fingers_splayed_tone1:': 'ðŸ–ï¸ðŸ»', + ':hand_splayed_tone1:': 'ðŸ–ï¸ðŸ»', + ':raised_hand_with_fingers_splayed_tone2:': 'ðŸ–ï¸ðŸ¼', + ':hand_splayed_tone2:': 'ðŸ–ï¸ðŸ¼', + ':raised_hand_with_fingers_splayed_tone3:': 'ðŸ–ï¸ðŸ½', + ':hand_splayed_tone3:': 'ðŸ–ï¸ðŸ½', + ':raised_hand_with_fingers_splayed_tone4:': 'ðŸ–ï¸ðŸ¾', + ':hand_splayed_tone4:': 'ðŸ–ï¸ðŸ¾', + ':raised_hand_with_fingers_splayed_tone5:': 'ðŸ–ï¸ðŸ¿', + ':hand_splayed_tone5:': 'ðŸ–ï¸ðŸ¿', + ':horse_racing_tone1:': 'ðŸ‡ðŸ»', + ':horse_racing_tone2:': 'ðŸ‡ðŸ¼', + ':horse_racing_tone3:': 'ðŸ‡ðŸ½', + ':horse_racing_tone4:': 'ðŸ‡ðŸ¾', + ':horse_racing_tone5:': 'ðŸ‡ðŸ¿', + ':left_fist_tone1:': '🤛ðŸ»', + ':left_facing_fist_tone1:': '🤛ðŸ»', + ':left_fist_tone2:': '🤛ðŸ¼', + ':left_facing_fist_tone2:': '🤛ðŸ¼', + ':left_fist_tone3:': '🤛ðŸ½', + ':left_facing_fist_tone3:': '🤛ðŸ½', + ':left_fist_tone4:': '🤛ðŸ¾', + ':left_facing_fist_tone4:': '🤛ðŸ¾', + ':left_fist_tone5:': '🤛ðŸ¿', + ':left_facing_fist_tone5:': '🤛ðŸ¿', + ':leg_light_skin_tone:': '🦵ðŸ»', + ':leg_tone1:': '🦵ðŸ»', + ':leg_medium_light_skin_tone:': '🦵ðŸ¼', + ':leg_tone2:': '🦵ðŸ¼', + ':leg_medium_skin_tone:': '🦵ðŸ½', + ':leg_tone3:': '🦵ðŸ½', + ':leg_medium_dark_skin_tone:': '🦵ðŸ¾', + ':leg_tone4:': '🦵ðŸ¾', + ':leg_dark_skin_tone:': '🦵ðŸ¿', + ':leg_tone5:': '🦵ðŸ¿', + ':man_in_business_suit_levitating_tone1:': '🕴ï¸ðŸ»', + ':man_in_business_suit_levitating_light_skin_tone:': '🕴ï¸ðŸ»', + ':levitate_tone1:': '🕴ï¸ðŸ»', + ':man_in_business_suit_levitating_tone2:': '🕴ï¸ðŸ¼', + ':man_in_business_suit_levitating_medium_light_skin_tone:': '🕴ï¸ðŸ¼', + ':levitate_tone2:': '🕴ï¸ðŸ¼', + ':man_in_business_suit_levitating_tone3:': '🕴ï¸ðŸ½', + ':man_in_business_suit_levitating_medium_skin_tone:': '🕴ï¸ðŸ½', + ':levitate_tone3:': '🕴ï¸ðŸ½', + ':man_in_business_suit_levitating_tone4:': '🕴ï¸ðŸ¾', + ':man_in_business_suit_levitating_medium_dark_skin_tone:': '🕴ï¸ðŸ¾', + ':levitate_tone4:': '🕴ï¸ðŸ¾', + ':man_in_business_suit_levitating_tone5:': '🕴ï¸ðŸ¿', + ':man_in_business_suit_levitating_dark_skin_tone:': '🕴ï¸ðŸ¿', + ':levitate_tone5:': '🕴ï¸ðŸ¿', + ':love_you_gesture_light_skin_tone:': '🤟ðŸ»', + ':love_you_gesture_tone1:': '🤟ðŸ»', + ':love_you_gesture_medium_light_skin_tone:': '🤟ðŸ¼', + ':love_you_gesture_tone2:': '🤟ðŸ¼', + ':love_you_gesture_medium_skin_tone:': '🤟ðŸ½', + ':love_you_gesture_tone3:': '🤟ðŸ½', + ':love_you_gesture_medium_dark_skin_tone:': '🤟ðŸ¾', + ':love_you_gesture_tone4:': '🤟ðŸ¾', + ':love_you_gesture_dark_skin_tone:': '🤟ðŸ¿', + ':love_you_gesture_tone5:': '🤟ðŸ¿', + ':mage_light_skin_tone:': '🧙ðŸ»', + ':mage_tone1:': '🧙ðŸ»', + ':mage_medium_light_skin_tone:': '🧙ðŸ¼', + ':mage_tone2:': '🧙ðŸ¼', + ':mage_medium_skin_tone:': '🧙ðŸ½', + ':mage_tone3:': '🧙ðŸ½', + ':mage_medium_dark_skin_tone:': '🧙ðŸ¾', + ':mage_tone4:': '🧙ðŸ¾', + ':mage_dark_skin_tone:': '🧙ðŸ¿', + ':mage_tone5:': '🧙ðŸ¿', + ':man_artist:': '👨â€ðŸŽ¨', + ':man_astronaut:': '👨â€ðŸš€', + ':man_bald:': '👨â€ðŸ¦²', + ':man_cook:': '👨â€ðŸ³', + ':man_curly_haired:': '👨â€ðŸ¦±', + ':male_dancer_tone1:': '🕺ðŸ»', + ':man_dancing_tone1:': '🕺ðŸ»', + ':male_dancer_tone2:': '🕺ðŸ¼', + ':man_dancing_tone2:': '🕺ðŸ¼', + ':male_dancer_tone3:': '🕺ðŸ½', + ':man_dancing_tone3:': '🕺ðŸ½', + ':male_dancer_tone4:': '🕺ðŸ¾', + ':man_dancing_tone4:': '🕺ðŸ¾', + ':male_dancer_tone5:': '🕺ðŸ¿', + ':man_dancing_tone5:': '🕺ðŸ¿', + ':man_factory_worker:': '👨â€ðŸ', + ':man_farmer:': '👨â€ðŸŒ¾', + ':man_firefighter:': '👨â€ðŸš’', + ':man_in_manual_wheelchair:': '👨â€ðŸ¦½', + ':man_in_motorized_wheelchair:': '👨â€ðŸ¦¼', + ':tuxedo_tone1:': '🤵ðŸ»', + ':man_in_tuxedo_tone1:': '🤵ðŸ»', + ':tuxedo_tone2:': '🤵ðŸ¼', + ':man_in_tuxedo_tone2:': '🤵ðŸ¼', + ':tuxedo_tone3:': '🤵ðŸ½', + ':man_in_tuxedo_tone3:': '🤵ðŸ½', + ':tuxedo_tone4:': '🤵ðŸ¾', + ':man_in_tuxedo_tone4:': '🤵ðŸ¾', + ':tuxedo_tone5:': '🤵ðŸ¿', + ':man_in_tuxedo_tone5:': '🤵ðŸ¿', + ':man_mechanic:': '👨â€ðŸ”§', + ':man_office_worker:': '👨â€ðŸ’¼', + ':man_red_haired:': '👨â€ðŸ¦°', + ':man_scientist:': '👨â€ðŸ”¬', + ':man_singer:': '👨â€ðŸŽ¤', + ':man_student:': '👨â€ðŸŽ“', + ':man_teacher:': '👨â€ðŸ«', + ':man_technologist:': '👨â€ðŸ’»', + ':man_tone1:': '👨ðŸ»', + ':man_tone2:': '👨ðŸ¼', + ':man_tone3:': '👨ðŸ½', + ':man_tone4:': '👨ðŸ¾', + ':man_tone5:': '👨ðŸ¿', + ':man_white_haired:': '👨â€ðŸ¦³', + ':man_with_gua_pi_mao_tone1:': '👲ðŸ»', + ':man_with_chinese_cap_tone1:': '👲ðŸ»', + ':man_with_gua_pi_mao_tone2:': '👲ðŸ¼', + ':man_with_chinese_cap_tone2:': '👲ðŸ¼', + ':man_with_gua_pi_mao_tone3:': '👲ðŸ½', + ':man_with_chinese_cap_tone3:': '👲ðŸ½', + ':man_with_gua_pi_mao_tone4:': '👲ðŸ¾', + ':man_with_chinese_cap_tone4:': '👲ðŸ¾', + ':man_with_gua_pi_mao_tone5:': '👲ðŸ¿', + ':man_with_chinese_cap_tone5:': '👲ðŸ¿', + ':man_with_probing_cane:': '👨â€ðŸ¦¯', + ':men_holding_hands_light_skin_tone:': '👬ðŸ»', + ':men_holding_hands_tone1:': '👬ðŸ»', + ':men_holding_hands_medium_light_skin_tone:': '👬ðŸ¼', + ':men_holding_hands_tone2:': '👬ðŸ¼', + ':men_holding_hands_medium_skin_tone:': '👬ðŸ½', + ':men_holding_hands_tone3:': '👬ðŸ½', + ':men_holding_hands_medium_dark_skin_tone:': '👬ðŸ¾', + ':men_holding_hands_tone4:': '👬ðŸ¾', + ':men_holding_hands_dark_skin_tone:': '👬ðŸ¿', + ':men_holding_hands_tone5:': '👬ðŸ¿', + ':merperson_light_skin_tone:': '🧜ðŸ»', + ':merperson_tone1:': '🧜ðŸ»', + ':merperson_medium_light_skin_tone:': '🧜ðŸ¼', + ':merperson_tone2:': '🧜ðŸ¼', + ':merperson_medium_skin_tone:': '🧜ðŸ½', + ':merperson_tone3:': '🧜ðŸ½', + ':merperson_medium_dark_skin_tone:': '🧜ðŸ¾', + ':merperson_tone4:': '🧜ðŸ¾', + ':merperson_dark_skin_tone:': '🧜ðŸ¿', + ':merperson_tone5:': '🧜ðŸ¿', + ':sign_of_the_horns_tone1:': '🤘ðŸ»', + ':metal_tone1:': '🤘ðŸ»', + ':sign_of_the_horns_tone2:': '🤘ðŸ¼', + ':metal_tone2:': '🤘ðŸ¼', + ':sign_of_the_horns_tone3:': '🤘ðŸ½', + ':metal_tone3:': '🤘ðŸ½', + ':sign_of_the_horns_tone4:': '🤘ðŸ¾', + ':metal_tone4:': '🤘ðŸ¾', + ':sign_of_the_horns_tone5:': '🤘ðŸ¿', + ':metal_tone5:': '🤘ðŸ¿', + ':reversed_hand_with_middle_finger_extended_tone1:': '🖕ðŸ»', + ':middle_finger_tone1:': '🖕ðŸ»', + ':reversed_hand_with_middle_finger_extended_tone2:': '🖕ðŸ¼', + ':middle_finger_tone2:': '🖕ðŸ¼', + ':reversed_hand_with_middle_finger_extended_tone3:': '🖕ðŸ½', + ':middle_finger_tone3:': '🖕ðŸ½', + ':reversed_hand_with_middle_finger_extended_tone4:': '🖕ðŸ¾', + ':middle_finger_tone4:': '🖕ðŸ¾', + ':reversed_hand_with_middle_finger_extended_tone5:': '🖕ðŸ¿', + ':middle_finger_tone5:': '🖕ðŸ¿', + ':mother_christmas_tone1:': '🤶ðŸ»', + ':mrs_claus_tone1:': '🤶ðŸ»', + ':mother_christmas_tone2:': '🤶ðŸ¼', + ':mrs_claus_tone2:': '🤶ðŸ¼', + ':mother_christmas_tone3:': '🤶ðŸ½', + ':mrs_claus_tone3:': '🤶ðŸ½', + ':mother_christmas_tone4:': '🤶ðŸ¾', + ':mrs_claus_tone4:': '🤶ðŸ¾', + ':mother_christmas_tone5:': '🤶ðŸ¿', + ':mrs_claus_tone5:': '🤶ðŸ¿', + ':muscle_tone1:': '💪ðŸ»', + ':muscle_tone2:': '💪ðŸ¼', + ':muscle_tone3:': '💪ðŸ½', + ':muscle_tone4:': '💪ðŸ¾', + ':muscle_tone5:': '💪ðŸ¿', + ':nail_care_tone1:': '💅ðŸ»', + ':nail_care_tone2:': '💅ðŸ¼', + ':nail_care_tone3:': '💅ðŸ½', + ':nail_care_tone4:': '💅ðŸ¾', + ':nail_care_tone5:': '💅ðŸ¿', + ':nose_tone1:': '👃ðŸ»', + ':nose_tone2:': '👃ðŸ¼', + ':nose_tone3:': '👃ðŸ½', + ':nose_tone4:': '👃ðŸ¾', + ':nose_tone5:': '👃ðŸ¿', + ':ok_hand_tone1:': '👌ðŸ»', + ':ok_hand_tone2:': '👌ðŸ¼', + ':ok_hand_tone3:': '👌ðŸ½', + ':ok_hand_tone4:': '👌ðŸ¾', + ':ok_hand_tone5:': '👌ðŸ¿', + ':older_adult_light_skin_tone:': '🧓ðŸ»', + ':older_adult_tone1:': '🧓ðŸ»', + ':older_adult_medium_light_skin_tone:': '🧓ðŸ¼', + ':older_adult_tone2:': '🧓ðŸ¼', + ':older_adult_medium_skin_tone:': '🧓ðŸ½', + ':older_adult_tone3:': '🧓ðŸ½', + ':older_adult_medium_dark_skin_tone:': '🧓ðŸ¾', + ':older_adult_tone4:': '🧓ðŸ¾', + ':older_adult_dark_skin_tone:': '🧓ðŸ¿', + ':older_adult_tone5:': '🧓ðŸ¿', + ':older_man_tone1:': '👴ðŸ»', + ':older_man_tone2:': '👴ðŸ¼', + ':older_man_tone3:': '👴ðŸ½', + ':older_man_tone4:': '👴ðŸ¾', + ':older_man_tone5:': '👴ðŸ¿', + ':grandma_tone1:': '👵ðŸ»', + ':older_woman_tone1:': '👵ðŸ»', + ':grandma_tone2:': '👵ðŸ¼', + ':older_woman_tone2:': '👵ðŸ¼', + ':grandma_tone3:': '👵ðŸ½', + ':older_woman_tone3:': '👵ðŸ½', + ':grandma_tone4:': '👵ðŸ¾', + ':older_woman_tone4:': '👵ðŸ¾', + ':grandma_tone5:': '👵ðŸ¿', + ':older_woman_tone5:': '👵ðŸ¿', + ':open_hands_tone1:': 'ðŸ‘ðŸ»', + ':open_hands_tone2:': 'ðŸ‘ðŸ¼', + ':open_hands_tone3:': 'ðŸ‘ðŸ½', + ':open_hands_tone4:': 'ðŸ‘ðŸ¾', + ':open_hands_tone5:': 'ðŸ‘ðŸ¿', + ':palms_up_together_light_skin_tone:': '🤲ðŸ»', + ':palms_up_together_tone1:': '🤲ðŸ»', + ':palms_up_together_medium_light_skin_tone:': '🤲ðŸ¼', + ':palms_up_together_tone2:': '🤲ðŸ¼', + ':palms_up_together_medium_skin_tone:': '🤲ðŸ½', + ':palms_up_together_tone3:': '🤲ðŸ½', + ':palms_up_together_medium_dark_skin_tone:': '🤲ðŸ¾', + ':palms_up_together_tone4:': '🤲ðŸ¾', + ':palms_up_together_dark_skin_tone:': '🤲ðŸ¿', + ':palms_up_together_tone5:': '🤲ðŸ¿', + ':bicyclist_tone1:': '🚴ðŸ»', + ':person_biking_tone1:': '🚴ðŸ»', + ':bicyclist_tone2:': '🚴ðŸ¼', + ':person_biking_tone2:': '🚴ðŸ¼', + ':bicyclist_tone3:': '🚴ðŸ½', + ':person_biking_tone3:': '🚴ðŸ½', + ':bicyclist_tone4:': '🚴ðŸ¾', + ':person_biking_tone4:': '🚴ðŸ¾', + ':bicyclist_tone5:': '🚴ðŸ¿', + ':person_biking_tone5:': '🚴ðŸ¿', + ':bow_tone1:': '🙇ðŸ»', + ':person_bowing_tone1:': '🙇ðŸ»', + ':bow_tone2:': '🙇ðŸ¼', + ':person_bowing_tone2:': '🙇ðŸ¼', + ':bow_tone3:': '🙇ðŸ½', + ':person_bowing_tone3:': '🙇ðŸ½', + ':bow_tone4:': '🙇ðŸ¾', + ':person_bowing_tone4:': '🙇ðŸ¾', + ':bow_tone5:': '🙇ðŸ¿', + ':person_bowing_tone5:': '🙇ðŸ¿', + ':person_climbing_light_skin_tone:': '🧗ðŸ»', + ':person_climbing_tone1:': '🧗ðŸ»', + ':person_climbing_medium_light_skin_tone:': '🧗ðŸ¼', + ':person_climbing_tone2:': '🧗ðŸ¼', + ':person_climbing_medium_skin_tone:': '🧗ðŸ½', + ':person_climbing_tone3:': '🧗ðŸ½', + ':person_climbing_medium_dark_skin_tone:': '🧗ðŸ¾', + ':person_climbing_tone4:': '🧗ðŸ¾', + ':person_climbing_dark_skin_tone:': '🧗ðŸ¿', + ':person_climbing_tone5:': '🧗ðŸ¿', + ':cartwheel_tone1:': '🤸ðŸ»', + ':person_doing_cartwheel_tone1:': '🤸ðŸ»', + ':cartwheel_tone2:': '🤸ðŸ¼', + ':person_doing_cartwheel_tone2:': '🤸ðŸ¼', + ':cartwheel_tone3:': '🤸ðŸ½', + ':person_doing_cartwheel_tone3:': '🤸ðŸ½', + ':cartwheel_tone4:': '🤸ðŸ¾', + ':person_doing_cartwheel_tone4:': '🤸ðŸ¾', + ':cartwheel_tone5:': '🤸ðŸ¿', + ':person_doing_cartwheel_tone5:': '🤸ðŸ¿', + ':face_palm_tone1:': '🤦ðŸ»', + ':facepalm_tone1:': '🤦ðŸ»', + ':person_facepalming_tone1:': '🤦ðŸ»', + ':face_palm_tone2:': '🤦ðŸ¼', + ':facepalm_tone2:': '🤦ðŸ¼', + ':person_facepalming_tone2:': '🤦ðŸ¼', + ':face_palm_tone3:': '🤦ðŸ½', + ':facepalm_tone3:': '🤦ðŸ½', + ':person_facepalming_tone3:': '🤦ðŸ½', + ':face_palm_tone4:': '🤦ðŸ¾', + ':facepalm_tone4:': '🤦ðŸ¾', + ':person_facepalming_tone4:': '🤦ðŸ¾', + ':face_palm_tone5:': '🤦ðŸ¿', + ':facepalm_tone5:': '🤦ðŸ¿', + ':person_facepalming_tone5:': '🤦ðŸ¿', + ':person_frowning_tone1:': 'ðŸ™ðŸ»', + ':person_frowning_tone2:': 'ðŸ™ðŸ¼', + ':person_frowning_tone3:': 'ðŸ™ðŸ½', + ':person_frowning_tone4:': 'ðŸ™ðŸ¾', + ':person_frowning_tone5:': 'ðŸ™ðŸ¿', + ':no_good_tone1:': '🙅ðŸ»', + ':person_gesturing_no_tone1:': '🙅ðŸ»', + ':no_good_tone2:': '🙅ðŸ¼', + ':person_gesturing_no_tone2:': '🙅ðŸ¼', + ':no_good_tone3:': '🙅ðŸ½', + ':person_gesturing_no_tone3:': '🙅ðŸ½', + ':no_good_tone4:': '🙅ðŸ¾', + ':person_gesturing_no_tone4:': '🙅ðŸ¾', + ':no_good_tone5:': '🙅ðŸ¿', + ':person_gesturing_no_tone5:': '🙅ðŸ¿', + ':ok_woman_tone1:': '🙆ðŸ»', + ':person_gesturing_ok_tone1:': '🙆ðŸ»', + ':ok_woman_tone2:': '🙆ðŸ¼', + ':person_gesturing_ok_tone2:': '🙆ðŸ¼', + ':ok_woman_tone3:': '🙆ðŸ½', + ':person_gesturing_ok_tone3:': '🙆ðŸ½', + ':ok_woman_tone4:': '🙆ðŸ¾', + ':person_gesturing_ok_tone4:': '🙆ðŸ¾', + ':ok_woman_tone5:': '🙆ðŸ¿', + ':person_gesturing_ok_tone5:': '🙆ðŸ¿', + ':haircut_tone1:': '💇ðŸ»', + ':person_getting_haircut_tone1:': '💇ðŸ»', + ':haircut_tone2:': '💇ðŸ¼', + ':person_getting_haircut_tone2:': '💇ðŸ¼', + ':haircut_tone3:': '💇ðŸ½', + ':person_getting_haircut_tone3:': '💇ðŸ½', + ':haircut_tone4:': '💇ðŸ¾', + ':person_getting_haircut_tone4:': '💇ðŸ¾', + ':haircut_tone5:': '💇ðŸ¿', + ':person_getting_haircut_tone5:': '💇ðŸ¿', + ':massage_tone1:': '💆ðŸ»', + ':person_getting_massage_tone1:': '💆ðŸ»', + ':massage_tone2:': '💆ðŸ¼', + ':person_getting_massage_tone2:': '💆ðŸ¼', + ':massage_tone3:': '💆ðŸ½', + ':person_getting_massage_tone3:': '💆ðŸ½', + ':massage_tone4:': '💆ðŸ¾', + ':person_getting_massage_tone4:': '💆ðŸ¾', + ':massage_tone5:': '💆ðŸ¿', + ':person_getting_massage_tone5:': '💆ðŸ¿', + ':person_golfing_light_skin_tone:': 'ðŸŒï¸ðŸ»', + ':person_golfing_tone1:': 'ðŸŒï¸ðŸ»', + ':person_golfing_medium_light_skin_tone:': 'ðŸŒï¸ðŸ¼', + ':person_golfing_tone2:': 'ðŸŒï¸ðŸ¼', + ':person_golfing_medium_skin_tone:': 'ðŸŒï¸ðŸ½', + ':person_golfing_tone3:': 'ðŸŒï¸ðŸ½', + ':person_golfing_medium_dark_skin_tone:': 'ðŸŒï¸ðŸ¾', + ':person_golfing_tone4:': 'ðŸŒï¸ðŸ¾', + ':person_golfing_dark_skin_tone:': 'ðŸŒï¸ðŸ¿', + ':person_golfing_tone5:': 'ðŸŒï¸ðŸ¿', + ':person_in_bed_light_skin_tone:': '🛌ðŸ»', + ':person_in_bed_tone1:': '🛌ðŸ»', + ':person_in_bed_medium_light_skin_tone:': '🛌ðŸ¼', + ':person_in_bed_tone2:': '🛌ðŸ¼', + ':person_in_bed_medium_skin_tone:': '🛌ðŸ½', + ':person_in_bed_tone3:': '🛌ðŸ½', + ':person_in_bed_medium_dark_skin_tone:': '🛌ðŸ¾', + ':person_in_bed_tone4:': '🛌ðŸ¾', + ':person_in_bed_dark_skin_tone:': '🛌ðŸ¿', + ':person_in_bed_tone5:': '🛌ðŸ¿', + ':person_in_lotus_position_light_skin_tone:': '🧘ðŸ»', + ':person_in_lotus_position_tone1:': '🧘ðŸ»', + ':person_in_lotus_position_medium_light_skin_tone:': '🧘ðŸ¼', + ':person_in_lotus_position_tone2:': '🧘ðŸ¼', + ':person_in_lotus_position_medium_skin_tone:': '🧘ðŸ½', + ':person_in_lotus_position_tone3:': '🧘ðŸ½', + ':person_in_lotus_position_medium_dark_skin_tone:': '🧘ðŸ¾', + ':person_in_lotus_position_tone4:': '🧘ðŸ¾', + ':person_in_lotus_position_dark_skin_tone:': '🧘ðŸ¿', + ':person_in_lotus_position_tone5:': '🧘ðŸ¿', + ':person_in_steamy_room_light_skin_tone:': '🧖ðŸ»', + ':person_in_steamy_room_tone1:': '🧖ðŸ»', + ':person_in_steamy_room_medium_light_skin_tone:': '🧖ðŸ¼', + ':person_in_steamy_room_tone2:': '🧖ðŸ¼', + ':person_in_steamy_room_medium_skin_tone:': '🧖ðŸ½', + ':person_in_steamy_room_tone3:': '🧖ðŸ½', + ':person_in_steamy_room_medium_dark_skin_tone:': '🧖ðŸ¾', + ':person_in_steamy_room_tone4:': '🧖ðŸ¾', + ':person_in_steamy_room_dark_skin_tone:': '🧖ðŸ¿', + ':person_in_steamy_room_tone5:': '🧖ðŸ¿', + ':juggling_tone1:': '🤹ðŸ»', + ':juggler_tone1:': '🤹ðŸ»', + ':person_juggling_tone1:': '🤹ðŸ»', + ':juggling_tone2:': '🤹ðŸ¼', + ':juggler_tone2:': '🤹ðŸ¼', + ':person_juggling_tone2:': '🤹ðŸ¼', + ':juggling_tone3:': '🤹ðŸ½', + ':juggler_tone3:': '🤹ðŸ½', + ':person_juggling_tone3:': '🤹ðŸ½', + ':juggling_tone4:': '🤹ðŸ¾', + ':juggler_tone4:': '🤹ðŸ¾', + ':person_juggling_tone4:': '🤹ðŸ¾', + ':juggling_tone5:': '🤹ðŸ¿', + ':juggler_tone5:': '🤹ðŸ¿', + ':person_juggling_tone5:': '🤹ðŸ¿', + ':person_kneeling_light_skin_tone:': '🧎ðŸ»', + ':person_kneeling_tone1:': '🧎ðŸ»', + ':person_kneeling_medium_light_skin_tone:': '🧎ðŸ¼', + ':person_kneeling_tone2:': '🧎ðŸ¼', + ':person_kneeling_medium_skin_tone:': '🧎ðŸ½', + ':person_kneeling_tone3:': '🧎ðŸ½', + ':person_kneeling_medium_dark_skin_tone:': '🧎ðŸ¾', + ':person_kneeling_tone4:': '🧎ðŸ¾', + ':person_kneeling_dark_skin_tone:': '🧎ðŸ¿', + ':person_kneeling_tone5:': '🧎ðŸ¿', + ':lifter_tone1:': 'ðŸ‹ï¸ðŸ»', + ':weight_lifter_tone1:': 'ðŸ‹ï¸ðŸ»', + ':person_lifting_weights_tone1:': 'ðŸ‹ï¸ðŸ»', + ':lifter_tone2:': 'ðŸ‹ï¸ðŸ¼', + ':weight_lifter_tone2:': 'ðŸ‹ï¸ðŸ¼', + ':person_lifting_weights_tone2:': 'ðŸ‹ï¸ðŸ¼', + ':lifter_tone3:': 'ðŸ‹ï¸ðŸ½', + ':weight_lifter_tone3:': 'ðŸ‹ï¸ðŸ½', + ':person_lifting_weights_tone3:': 'ðŸ‹ï¸ðŸ½', + ':lifter_tone4:': 'ðŸ‹ï¸ðŸ¾', + ':weight_lifter_tone4:': 'ðŸ‹ï¸ðŸ¾', + ':person_lifting_weights_tone4:': 'ðŸ‹ï¸ðŸ¾', + ':lifter_tone5:': 'ðŸ‹ï¸ðŸ¿', + ':weight_lifter_tone5:': 'ðŸ‹ï¸ðŸ¿', + ':person_lifting_weights_tone5:': 'ðŸ‹ï¸ðŸ¿', + ':mountain_bicyclist_tone1:': '🚵ðŸ»', + ':person_mountain_biking_tone1:': '🚵ðŸ»', + ':mountain_bicyclist_tone2:': '🚵ðŸ¼', + ':person_mountain_biking_tone2:': '🚵ðŸ¼', + ':mountain_bicyclist_tone3:': '🚵ðŸ½', + ':person_mountain_biking_tone3:': '🚵ðŸ½', + ':mountain_bicyclist_tone4:': '🚵ðŸ¾', + ':person_mountain_biking_tone4:': '🚵ðŸ¾', + ':mountain_bicyclist_tone5:': '🚵ðŸ¿', + ':person_mountain_biking_tone5:': '🚵ðŸ¿', + ':handball_tone1:': '🤾ðŸ»', + ':person_playing_handball_tone1:': '🤾ðŸ»', + ':handball_tone2:': '🤾ðŸ¼', + ':person_playing_handball_tone2:': '🤾ðŸ¼', + ':handball_tone3:': '🤾ðŸ½', + ':person_playing_handball_tone3:': '🤾ðŸ½', + ':handball_tone4:': '🤾ðŸ¾', + ':person_playing_handball_tone4:': '🤾ðŸ¾', + ':handball_tone5:': '🤾ðŸ¿', + ':person_playing_handball_tone5:': '🤾ðŸ¿', + ':water_polo_tone1:': '🤽ðŸ»', + ':person_playing_water_polo_tone1:': '🤽ðŸ»', + ':water_polo_tone2:': '🤽ðŸ¼', + ':person_playing_water_polo_tone2:': '🤽ðŸ¼', + ':water_polo_tone3:': '🤽ðŸ½', + ':person_playing_water_polo_tone3:': '🤽ðŸ½', + ':water_polo_tone4:': '🤽ðŸ¾', + ':person_playing_water_polo_tone4:': '🤽ðŸ¾', + ':water_polo_tone5:': '🤽ðŸ¿', + ':person_playing_water_polo_tone5:': '🤽ðŸ¿', + ':person_with_pouting_face_tone1:': '🙎ðŸ»', + ':person_pouting_tone1:': '🙎ðŸ»', + ':person_with_pouting_face_tone2:': '🙎ðŸ¼', + ':person_pouting_tone2:': '🙎ðŸ¼', + ':person_with_pouting_face_tone3:': '🙎ðŸ½', + ':person_pouting_tone3:': '🙎ðŸ½', + ':person_with_pouting_face_tone4:': '🙎ðŸ¾', + ':person_pouting_tone4:': '🙎ðŸ¾', + ':person_with_pouting_face_tone5:': '🙎ðŸ¿', + ':person_pouting_tone5:': '🙎ðŸ¿', + ':raising_hand_tone1:': '🙋ðŸ»', + ':person_raising_hand_tone1:': '🙋ðŸ»', + ':raising_hand_tone2:': '🙋ðŸ¼', + ':person_raising_hand_tone2:': '🙋ðŸ¼', + ':raising_hand_tone3:': '🙋ðŸ½', + ':person_raising_hand_tone3:': '🙋ðŸ½', + ':raising_hand_tone4:': '🙋ðŸ¾', + ':person_raising_hand_tone4:': '🙋ðŸ¾', + ':raising_hand_tone5:': '🙋ðŸ¿', + ':person_raising_hand_tone5:': '🙋ðŸ¿', + ':rowboat_tone1:': '🚣ðŸ»', + ':person_rowing_boat_tone1:': '🚣ðŸ»', + ':rowboat_tone2:': '🚣ðŸ¼', + ':person_rowing_boat_tone2:': '🚣ðŸ¼', + ':rowboat_tone3:': '🚣ðŸ½', + ':person_rowing_boat_tone3:': '🚣ðŸ½', + ':rowboat_tone4:': '🚣ðŸ¾', + ':person_rowing_boat_tone4:': '🚣ðŸ¾', + ':rowboat_tone5:': '🚣ðŸ¿', + ':person_rowing_boat_tone5:': '🚣ðŸ¿', + ':runner_tone1:': 'ðŸƒðŸ»', + ':person_running_tone1:': 'ðŸƒðŸ»', + ':runner_tone2:': 'ðŸƒðŸ¼', + ':person_running_tone2:': 'ðŸƒðŸ¼', + ':runner_tone3:': 'ðŸƒðŸ½', + ':person_running_tone3:': 'ðŸƒðŸ½', + ':runner_tone4:': 'ðŸƒðŸ¾', + ':person_running_tone4:': 'ðŸƒðŸ¾', + ':runner_tone5:': 'ðŸƒðŸ¿', + ':person_running_tone5:': 'ðŸƒðŸ¿', + ':shrug_tone1:': '🤷ðŸ»', + ':person_shrugging_tone1:': '🤷ðŸ»', + ':shrug_tone2:': '🤷ðŸ¼', + ':person_shrugging_tone2:': '🤷ðŸ¼', + ':shrug_tone3:': '🤷ðŸ½', + ':person_shrugging_tone3:': '🤷ðŸ½', + ':shrug_tone4:': '🤷ðŸ¾', + ':person_shrugging_tone4:': '🤷ðŸ¾', + ':shrug_tone5:': '🤷ðŸ¿', + ':person_shrugging_tone5:': '🤷ðŸ¿', + ':person_standing_light_skin_tone:': 'ðŸ§ðŸ»', + ':person_standing_tone1:': 'ðŸ§ðŸ»', + ':person_standing_medium_light_skin_tone:': 'ðŸ§ðŸ¼', + ':person_standing_tone2:': 'ðŸ§ðŸ¼', + ':person_standing_medium_skin_tone:': 'ðŸ§ðŸ½', + ':person_standing_tone3:': 'ðŸ§ðŸ½', + ':person_standing_medium_dark_skin_tone:': 'ðŸ§ðŸ¾', + ':person_standing_tone4:': 'ðŸ§ðŸ¾', + ':person_standing_dark_skin_tone:': 'ðŸ§ðŸ¿', + ':person_standing_tone5:': 'ðŸ§ðŸ¿', + ':surfer_tone1:': 'ðŸ„ðŸ»', + ':person_surfing_tone1:': 'ðŸ„ðŸ»', + ':surfer_tone2:': 'ðŸ„ðŸ¼', + ':person_surfing_tone2:': 'ðŸ„ðŸ¼', + ':surfer_tone3:': 'ðŸ„ðŸ½', + ':person_surfing_tone3:': 'ðŸ„ðŸ½', + ':surfer_tone4:': 'ðŸ„ðŸ¾', + ':person_surfing_tone4:': 'ðŸ„ðŸ¾', + ':surfer_tone5:': 'ðŸ„ðŸ¿', + ':person_surfing_tone5:': 'ðŸ„ðŸ¿', + ':swimmer_tone1:': 'ðŸŠðŸ»', + ':person_swimming_tone1:': 'ðŸŠðŸ»', + ':swimmer_tone2:': 'ðŸŠðŸ¼', + ':person_swimming_tone2:': 'ðŸŠðŸ¼', + ':swimmer_tone3:': 'ðŸŠðŸ½', + ':person_swimming_tone3:': 'ðŸŠðŸ½', + ':swimmer_tone4:': 'ðŸŠðŸ¾', + ':person_swimming_tone4:': 'ðŸŠðŸ¾', + ':swimmer_tone5:': 'ðŸŠðŸ¿', + ':person_swimming_tone5:': 'ðŸŠðŸ¿', + ':information_desk_person_tone1:': 'ðŸ’ðŸ»', + ':person_tipping_hand_tone1:': 'ðŸ’ðŸ»', + ':information_desk_person_tone2:': 'ðŸ’ðŸ¼', + ':person_tipping_hand_tone2:': 'ðŸ’ðŸ¼', + ':information_desk_person_tone3:': 'ðŸ’ðŸ½', + ':person_tipping_hand_tone3:': 'ðŸ’ðŸ½', + ':information_desk_person_tone4:': 'ðŸ’ðŸ¾', + ':person_tipping_hand_tone4:': 'ðŸ’ðŸ¾', + ':information_desk_person_tone5:': 'ðŸ’ðŸ¿', + ':person_tipping_hand_tone5:': 'ðŸ’ðŸ¿', + ':walking_tone1:': '🚶ðŸ»', + ':person_walking_tone1:': '🚶ðŸ»', + ':walking_tone2:': '🚶ðŸ¼', + ':person_walking_tone2:': '🚶ðŸ¼', + ':walking_tone3:': '🚶ðŸ½', + ':person_walking_tone3:': '🚶ðŸ½', + ':walking_tone4:': '🚶ðŸ¾', + ':person_walking_tone4:': '🚶ðŸ¾', + ':walking_tone5:': '🚶ðŸ¿', + ':person_walking_tone5:': '🚶ðŸ¿', + ':man_with_turban_tone1:': '👳ðŸ»', + ':person_wearing_turban_tone1:': '👳ðŸ»', + ':man_with_turban_tone2:': '👳ðŸ¼', + ':person_wearing_turban_tone2:': '👳ðŸ¼', + ':man_with_turban_tone3:': '👳ðŸ½', + ':person_wearing_turban_tone3:': '👳ðŸ½', + ':man_with_turban_tone4:': '👳ðŸ¾', + ':person_wearing_turban_tone4:': '👳ðŸ¾', + ':man_with_turban_tone5:': '👳ðŸ¿', + ':person_wearing_turban_tone5:': '👳ðŸ¿', + ':pinching_hand_light_skin_tone:': 'ðŸ¤ðŸ»', + ':pinching_hand_tone1:': 'ðŸ¤ðŸ»', + ':pinching_hand_medium_light_skin_tone:': 'ðŸ¤ðŸ¼', + ':pinching_hand_tone2:': 'ðŸ¤ðŸ¼', + ':pinching_hand_medium_skin_tone:': 'ðŸ¤ðŸ½', + ':pinching_hand_tone3:': 'ðŸ¤ðŸ½', + ':pinching_hand_medium_dark_skin_tone:': 'ðŸ¤ðŸ¾', + ':pinching_hand_tone4:': 'ðŸ¤ðŸ¾', + ':pinching_hand_dark_skin_tone:': 'ðŸ¤ðŸ¿', + ':pinching_hand_tone5:': 'ðŸ¤ðŸ¿', + ':point_down_tone1:': '👇ðŸ»', + ':point_down_tone2:': '👇ðŸ¼', + ':point_down_tone3:': '👇ðŸ½', + ':point_down_tone4:': '👇ðŸ¾', + ':point_down_tone5:': '👇ðŸ¿', + ':point_left_tone1:': '👈ðŸ»', + ':point_left_tone2:': '👈ðŸ¼', + ':point_left_tone3:': '👈ðŸ½', + ':point_left_tone4:': '👈ðŸ¾', + ':point_left_tone5:': '👈ðŸ¿', + ':point_right_tone1:': '👉ðŸ»', + ':point_right_tone2:': '👉ðŸ¼', + ':point_right_tone3:': '👉ðŸ½', + ':point_right_tone4:': '👉ðŸ¾', + ':point_right_tone5:': '👉ðŸ¿', + ':point_up_2_tone1:': '👆ðŸ»', + ':point_up_2_tone2:': '👆ðŸ¼', + ':point_up_2_tone3:': '👆ðŸ½', + ':point_up_2_tone4:': '👆ðŸ¾', + ':point_up_2_tone5:': '👆ðŸ¿', + ':cop_tone1:': '👮ðŸ»', + ':police_officer_tone1:': '👮ðŸ»', + ':cop_tone2:': '👮ðŸ¼', + ':police_officer_tone2:': '👮ðŸ¼', + ':cop_tone3:': '👮ðŸ½', + ':police_officer_tone3:': '👮ðŸ½', + ':cop_tone4:': '👮ðŸ¾', + ':police_officer_tone4:': '👮ðŸ¾', + ':cop_tone5:': '👮ðŸ¿', + ':police_officer_tone5:': '👮ðŸ¿', + ':pray_tone1:': 'ðŸ™ðŸ»', + ':pray_tone2:': 'ðŸ™ðŸ¼', + ':pray_tone3:': 'ðŸ™ðŸ½', + ':pray_tone4:': 'ðŸ™ðŸ¾', + ':pray_tone5:': 'ðŸ™ðŸ¿', + ':expecting_woman_tone1:': '🤰ðŸ»', + ':pregnant_woman_tone1:': '🤰ðŸ»', + ':expecting_woman_tone2:': '🤰ðŸ¼', + ':pregnant_woman_tone2:': '🤰ðŸ¼', + ':expecting_woman_tone3:': '🤰ðŸ½', + ':pregnant_woman_tone3:': '🤰ðŸ½', + ':expecting_woman_tone4:': '🤰ðŸ¾', + ':pregnant_woman_tone4:': '🤰ðŸ¾', + ':expecting_woman_tone5:': '🤰ðŸ¿', + ':pregnant_woman_tone5:': '🤰ðŸ¿', + ':prince_tone1:': '🤴ðŸ»', + ':prince_tone2:': '🤴ðŸ¼', + ':prince_tone3:': '🤴ðŸ½', + ':prince_tone4:': '🤴ðŸ¾', + ':prince_tone5:': '🤴ðŸ¿', + ':princess_tone1:': '👸ðŸ»', + ':princess_tone2:': '👸ðŸ¼', + ':princess_tone3:': '👸ðŸ½', + ':princess_tone4:': '👸ðŸ¾', + ':princess_tone5:': '👸ðŸ¿', + ':punch_tone1:': '👊ðŸ»', + ':punch_tone2:': '👊ðŸ¼', + ':punch_tone3:': '👊ðŸ½', + ':punch_tone4:': '👊ðŸ¾', + ':punch_tone5:': '👊ðŸ¿', + ':gay_pride_flag:': 'ðŸ³ï¸â€ðŸŒˆ', + ':rainbow_flag:': 'ðŸ³ï¸â€ðŸŒˆ', + ':back_of_hand_tone1:': '🤚ðŸ»', + ':raised_back_of_hand_tone1:': '🤚ðŸ»', + ':back_of_hand_tone2:': '🤚ðŸ¼', + ':raised_back_of_hand_tone2:': '🤚ðŸ¼', + ':back_of_hand_tone3:': '🤚ðŸ½', + ':raised_back_of_hand_tone3:': '🤚ðŸ½', + ':back_of_hand_tone4:': '🤚ðŸ¾', + ':raised_back_of_hand_tone4:': '🤚ðŸ¾', + ':back_of_hand_tone5:': '🤚ðŸ¿', + ':raised_back_of_hand_tone5:': '🤚ðŸ¿', + ':raised_hands_tone1:': '🙌ðŸ»', + ':raised_hands_tone2:': '🙌ðŸ¼', + ':raised_hands_tone3:': '🙌ðŸ½', + ':raised_hands_tone4:': '🙌ðŸ¾', + ':raised_hands_tone5:': '🙌ðŸ¿', + ':right_fist_tone1:': '🤜ðŸ»', + ':right_facing_fist_tone1:': '🤜ðŸ»', + ':right_fist_tone2:': '🤜ðŸ¼', + ':right_facing_fist_tone2:': '🤜ðŸ¼', + ':right_fist_tone3:': '🤜ðŸ½', + ':right_facing_fist_tone3:': '🤜ðŸ½', + ':right_fist_tone4:': '🤜ðŸ¾', + ':right_facing_fist_tone4:': '🤜ðŸ¾', + ':right_fist_tone5:': '🤜ðŸ¿', + ':right_facing_fist_tone5:': '🤜ðŸ¿', + ':santa_tone1:': '🎅ðŸ»', + ':santa_tone2:': '🎅ðŸ¼', + ':santa_tone3:': '🎅ðŸ½', + ':santa_tone4:': '🎅ðŸ¾', + ':santa_tone5:': '🎅ðŸ¿', + ':selfie_tone1:': '🤳ðŸ»', + ':selfie_tone2:': '🤳ðŸ¼', + ':selfie_tone3:': '🤳ðŸ½', + ':selfie_tone4:': '🤳ðŸ¾', + ':selfie_tone5:': '🤳ðŸ¿', + ':service_dog:': 'ðŸ•â€ðŸ¦º', + ':snowboarder_light_skin_tone:': 'ðŸ‚ðŸ»', + ':snowboarder_tone1:': 'ðŸ‚ðŸ»', + ':snowboarder_medium_light_skin_tone:': 'ðŸ‚ðŸ¼', + ':snowboarder_tone2:': 'ðŸ‚ðŸ¼', + ':snowboarder_medium_skin_tone:': 'ðŸ‚ðŸ½', + ':snowboarder_tone3:': 'ðŸ‚ðŸ½', + ':snowboarder_medium_dark_skin_tone:': 'ðŸ‚ðŸ¾', + ':snowboarder_tone4:': 'ðŸ‚ðŸ¾', + ':snowboarder_dark_skin_tone:': 'ðŸ‚ðŸ¿', + ':snowboarder_tone5:': 'ðŸ‚ðŸ¿', + ':superhero_light_skin_tone:': '🦸ðŸ»', + ':superhero_tone1:': '🦸ðŸ»', + ':superhero_medium_light_skin_tone:': '🦸ðŸ¼', + ':superhero_tone2:': '🦸ðŸ¼', + ':superhero_medium_skin_tone:': '🦸ðŸ½', + ':superhero_tone3:': '🦸ðŸ½', + ':superhero_medium_dark_skin_tone:': '🦸ðŸ¾', + ':superhero_tone4:': '🦸ðŸ¾', + ':superhero_dark_skin_tone:': '🦸ðŸ¿', + ':superhero_tone5:': '🦸ðŸ¿', + ':supervillain_light_skin_tone:': '🦹ðŸ»', + ':supervillain_tone1:': '🦹ðŸ»', + ':supervillain_medium_light_skin_tone:': '🦹ðŸ¼', + ':supervillain_tone2:': '🦹ðŸ¼', + ':supervillain_medium_skin_tone:': '🦹ðŸ½', + ':supervillain_tone3:': '🦹ðŸ½', + ':supervillain_medium_dark_skin_tone:': '🦹ðŸ¾', + ':supervillain_tone4:': '🦹ðŸ¾', + ':supervillain_dark_skin_tone:': '🦹ðŸ¿', + ':supervillain_tone5:': '🦹ðŸ¿', + ':-1_tone1:': '👎ðŸ»', + ':thumbdown_tone1:': '👎ðŸ»', + ':thumbsdown_tone1:': '👎ðŸ»', + ':-1_tone2:': '👎ðŸ¼', + ':thumbdown_tone2:': '👎ðŸ¼', + ':thumbsdown_tone2:': '👎ðŸ¼', + ':-1_tone3:': '👎ðŸ½', + ':thumbdown_tone3:': '👎ðŸ½', + ':thumbsdown_tone3:': '👎ðŸ½', + ':-1_tone4:': '👎ðŸ¾', + ':thumbdown_tone4:': '👎ðŸ¾', + ':thumbsdown_tone4:': '👎ðŸ¾', + ':-1_tone5:': '👎ðŸ¿', + ':thumbdown_tone5:': '👎ðŸ¿', + ':thumbsdown_tone5:': '👎ðŸ¿', + ':+1_tone1:': 'ðŸ‘ðŸ»', + ':thumbup_tone1:': 'ðŸ‘ðŸ»', + ':thumbsup_tone1:': 'ðŸ‘ðŸ»', + ':+1_tone2:': 'ðŸ‘ðŸ¼', + ':thumbup_tone2:': 'ðŸ‘ðŸ¼', + ':thumbsup_tone2:': 'ðŸ‘ðŸ¼', + ':+1_tone3:': 'ðŸ‘ðŸ½', + ':thumbup_tone3:': 'ðŸ‘ðŸ½', + ':thumbsup_tone3:': 'ðŸ‘ðŸ½', + ':+1_tone4:': 'ðŸ‘ðŸ¾', + ':thumbup_tone4:': 'ðŸ‘ðŸ¾', + ':thumbsup_tone4:': 'ðŸ‘ðŸ¾', + ':+1_tone5:': 'ðŸ‘ðŸ¿', + ':thumbup_tone5:': 'ðŸ‘ðŸ¿', + ':thumbsup_tone5:': 'ðŸ‘ðŸ¿', + ':united_nations:': '🇺🇳', + ':vampire_light_skin_tone:': '🧛ðŸ»', + ':vampire_tone1:': '🧛ðŸ»', + ':vampire_medium_light_skin_tone:': '🧛ðŸ¼', + ':vampire_tone2:': '🧛ðŸ¼', + ':vampire_medium_skin_tone:': '🧛ðŸ½', + ':vampire_tone3:': '🧛ðŸ½', + ':vampire_medium_dark_skin_tone:': '🧛ðŸ¾', + ':vampire_tone4:': '🧛ðŸ¾', + ':vampire_dark_skin_tone:': '🧛ðŸ¿', + ':vampire_tone5:': '🧛ðŸ¿', + ':raised_hand_with_part_between_middle_and_ring_fingers_tone1:': '🖖ðŸ»', + ':vulcan_tone1:': '🖖ðŸ»', + ':raised_hand_with_part_between_middle_and_ring_fingers_tone2:': '🖖ðŸ¼', + ':vulcan_tone2:': '🖖ðŸ¼', + ':raised_hand_with_part_between_middle_and_ring_fingers_tone3:': '🖖ðŸ½', + ':vulcan_tone3:': '🖖ðŸ½', + ':raised_hand_with_part_between_middle_and_ring_fingers_tone4:': '🖖ðŸ¾', + ':vulcan_tone4:': '🖖ðŸ¾', + ':raised_hand_with_part_between_middle_and_ring_fingers_tone5:': '🖖ðŸ¿', + ':vulcan_tone5:': '🖖ðŸ¿', + ':wave_tone1:': '👋ðŸ»', + ':wave_tone2:': '👋ðŸ¼', + ':wave_tone3:': '👋ðŸ½', + ':wave_tone4:': '👋ðŸ¾', + ':wave_tone5:': '👋ðŸ¿', + ':woman_and_man_holding_hands_light_skin_tone:': '👫ðŸ»', + ':woman_and_man_holding_hands_tone1:': '👫ðŸ»', + ':woman_and_man_holding_hands_medium_light_skin_tone:': '👫ðŸ¼', + ':woman_and_man_holding_hands_tone2:': '👫ðŸ¼', + ':woman_and_man_holding_hands_medium_skin_tone:': '👫ðŸ½', + ':woman_and_man_holding_hands_tone3:': '👫ðŸ½', + ':woman_and_man_holding_hands_medium_dark_skin_tone:': '👫ðŸ¾', + ':woman_and_man_holding_hands_tone4:': '👫ðŸ¾', + ':woman_and_man_holding_hands_dark_skin_tone:': '👫ðŸ¿', + ':woman_and_man_holding_hands_tone5:': '👫ðŸ¿', + ':woman_artist:': '👩â€ðŸŽ¨', + ':woman_astronaut:': '👩â€ðŸš€', + ':woman_bald:': '👩â€ðŸ¦²', + ':woman_cook:': '👩â€ðŸ³', + ':woman_curly_haired:': '👩â€ðŸ¦±', + ':woman_factory_worker:': '👩â€ðŸ', + ':woman_farmer:': '👩â€ðŸŒ¾', + ':woman_firefighter:': '👩â€ðŸš’', + ':woman_in_manual_wheelchair:': '👩â€ðŸ¦½', + ':woman_in_motorized_wheelchair:': '👩â€ðŸ¦¼', + ':woman_mechanic:': '👩â€ðŸ”§', + ':woman_office_worker:': '👩â€ðŸ’¼', + ':woman_red_haired:': '👩â€ðŸ¦°', + ':woman_scientist:': '👩â€ðŸ”¬', + ':woman_singer:': '👩â€ðŸŽ¤', + ':woman_student:': '👩â€ðŸŽ“', + ':woman_teacher:': '👩â€ðŸ«', + ':woman_technologist:': '👩â€ðŸ’»', + ':woman_tone1:': '👩ðŸ»', + ':woman_tone2:': '👩ðŸ¼', + ':woman_tone3:': '👩ðŸ½', + ':woman_tone4:': '👩ðŸ¾', + ':woman_tone5:': '👩ðŸ¿', + ':woman_white_haired:': '👩â€ðŸ¦³', + ':woman_with_headscarf_light_skin_tone:': '🧕ðŸ»', + ':woman_with_headscarf_tone1:': '🧕ðŸ»', + ':woman_with_headscarf_medium_light_skin_tone:': '🧕ðŸ¼', + ':woman_with_headscarf_tone2:': '🧕ðŸ¼', + ':woman_with_headscarf_medium_skin_tone:': '🧕ðŸ½', + ':woman_with_headscarf_tone3:': '🧕ðŸ½', + ':woman_with_headscarf_medium_dark_skin_tone:': '🧕ðŸ¾', + ':woman_with_headscarf_tone4:': '🧕ðŸ¾', + ':woman_with_headscarf_dark_skin_tone:': '🧕ðŸ¿', + ':woman_with_headscarf_tone5:': '🧕ðŸ¿', + ':woman_with_probing_cane:': '👩â€ðŸ¦¯', + ':women_holding_hands_light_skin_tone:': 'ðŸ‘ðŸ»', + ':women_holding_hands_tone1:': 'ðŸ‘ðŸ»', + ':women_holding_hands_medium_light_skin_tone:': 'ðŸ‘ðŸ¼', + ':women_holding_hands_tone2:': 'ðŸ‘ðŸ¼', + ':women_holding_hands_medium_skin_tone:': 'ðŸ‘ðŸ½', + ':women_holding_hands_tone3:': 'ðŸ‘ðŸ½', + ':women_holding_hands_medium_dark_skin_tone:': 'ðŸ‘ðŸ¾', + ':women_holding_hands_tone4:': 'ðŸ‘ðŸ¾', + ':women_holding_hands_dark_skin_tone:': 'ðŸ‘ðŸ¿', + ':women_holding_hands_tone5:': 'ðŸ‘ðŸ¿', + ':blond-haired_man:': '👱â€â™‚ï¸', + ':blond-haired_woman:': '👱â€â™€ï¸', + ':deaf_man:': 'ðŸ§â€â™‚ï¸', + ':deaf_woman:': 'ðŸ§â€â™€ï¸', + ':fist_tone1:': '✊ðŸ»', + ':fist_tone2:': '✊ðŸ¼', + ':fist_tone3:': '✊ðŸ½', + ':fist_tone4:': '✊ðŸ¾', + ':fist_tone5:': '✊ðŸ¿', + ':man_biking:': '🚴â€â™‚ï¸', + ':man_bowing:': '🙇â€â™‚ï¸', + ':man_cartwheeling:': '🤸â€â™‚ï¸', + ':man_climbing:': '🧗â€â™‚ï¸', + ':man_construction_worker:': '👷â€â™‚ï¸', + ':man_detective:': '🕵ï¸â€â™‚ï¸', + ':man_elf:': 'ðŸ§â€â™‚ï¸', + ':man_facepalming:': '🤦â€â™‚ï¸', + ':man_fairy:': '🧚â€â™‚ï¸', + ':man_frowning:': 'ðŸ™â€â™‚ï¸', + ':man_genie:': '🧞â€â™‚ï¸', + ':man_gesturing_no:': '🙅â€â™‚ï¸', + ':man_gesturing_ok:': '🙆â€â™‚ï¸', + ':man_getting_face_massage:': '💆â€â™‚ï¸', + ':man_getting_haircut:': '💇â€â™‚ï¸', + ':man_golfing:': 'ðŸŒï¸â€â™‚ï¸', + ':man_guard:': '💂â€â™‚ï¸', + ':man_health_worker:': '👨â€âš•ï¸', + ':man_in_lotus_position:': '🧘â€â™‚ï¸', + ':man_in_steamy_room:': '🧖â€â™‚ï¸', + ':man_judge:': '👨â€âš–ï¸', + ':man_juggling:': '🤹â€â™‚ï¸', + ':man_kneeling:': '🧎â€â™‚ï¸', + ':man_lifting_weights:': 'ðŸ‹ï¸â€â™‚ï¸', + ':man_mage:': '🧙â€â™‚ï¸', + ':man_mountain_biking:': '🚵â€â™‚ï¸', + ':man_pilot:': '👨â€âœˆï¸', + ':man_playing_handball:': '🤾â€â™‚ï¸', + ':man_playing_water_polo:': '🤽â€â™‚ï¸', + ':man_police_officer:': '👮â€â™‚ï¸', + ':man_pouting:': '🙎â€â™‚ï¸', + ':man_raising_hand:': '🙋â€â™‚ï¸', + ':man_rowing_boat:': '🚣â€â™‚ï¸', + ':man_running:': 'ðŸƒâ€â™‚ï¸', + ':man_shrugging:': '🤷â€â™‚ï¸', + ':man_standing:': 'ðŸ§â€â™‚ï¸', + ':man_superhero:': '🦸â€â™‚ï¸', + ':man_supervillain:': '🦹â€â™‚ï¸', + ':man_surfing:': 'ðŸ„â€â™‚ï¸', + ':man_swimming:': 'ðŸŠâ€â™‚ï¸', + ':man_tipping_hand:': 'ðŸ’â€â™‚ï¸', + ':man_vampire:': '🧛â€â™‚ï¸', + ':man_walking:': '🚶â€â™‚ï¸', + ':man_wearing_turban:': '👳â€â™‚ï¸', + ':man_zombie:': '🧟â€â™‚ï¸', + ':men_with_bunny_ears_partying:': '👯â€â™‚ï¸', + ':men_wrestling:': '🤼♂ï¸', + ':mermaid:': '🧜â€â™€ï¸', + ':merman:': '🧜â€â™‚ï¸', + ':basketball_player_tone1:': '⛹ï¸ðŸ»', + ':person_with_ball_tone1:': '⛹ï¸ðŸ»', + ':person_bouncing_ball_tone1:': '⛹ï¸ðŸ»', + ':basketball_player_tone2:': '⛹ï¸ðŸ¼', + ':person_with_ball_tone2:': '⛹ï¸ðŸ¼', + ':person_bouncing_ball_tone2:': '⛹ï¸ðŸ¼', + ':basketball_player_tone3:': '⛹ï¸ðŸ½', + ':person_with_ball_tone3:': '⛹ï¸ðŸ½', + ':person_bouncing_ball_tone3:': '⛹ï¸ðŸ½', + ':basketball_player_tone4:': '⛹ï¸ðŸ¾', + ':person_with_ball_tone4:': '⛹ï¸ðŸ¾', + ':person_bouncing_ball_tone4:': '⛹ï¸ðŸ¾', + ':basketball_player_tone5:': '⛹ï¸ðŸ¿', + ':person_with_ball_tone5:': '⛹ï¸ðŸ¿', + ':person_bouncing_ball_tone5:': '⛹ï¸ðŸ¿', + ':pirate_flag:': 'ðŸ´â€â˜ ï¸', + ':point_up_tone1:': 'â˜ï¸ðŸ»', + ':point_up_tone2:': 'â˜ï¸ðŸ¼', + ':point_up_tone3:': 'â˜ï¸ðŸ½', + ':point_up_tone4:': 'â˜ï¸ðŸ¾', + ':point_up_tone5:': 'â˜ï¸ðŸ¿', + ':raised_hand_tone1:': '✋ðŸ»', + ':raised_hand_tone2:': '✋ðŸ¼', + ':raised_hand_tone3:': '✋ðŸ½', + ':raised_hand_tone4:': '✋ðŸ¾', + ':raised_hand_tone5:': '✋ðŸ¿', + ':v_tone1:': '✌ï¸ðŸ»', + ':v_tone2:': '✌ï¸ðŸ¼', + ':v_tone3:': '✌ï¸ðŸ½', + ':v_tone4:': '✌ï¸ðŸ¾', + ':v_tone5:': '✌ï¸ðŸ¿', + ':woman_biking:': '🚴â€â™€ï¸', + ':woman_bowing:': '🙇â€â™€ï¸', + ':woman_cartwheeling:': '🤸â€â™€ï¸', + ':woman_climbing:': '🧗â€â™€ï¸', + ':woman_construction_worker:': '👷â€â™€ï¸', + ':woman_detective:': '🕵ï¸â€â™€ï¸', + ':woman_elf:': 'ðŸ§â€â™€ï¸', + ':woman_facepalming:': '🤦â€â™€ï¸', + ':woman_fairy:': '🧚â€â™€ï¸', + ':woman_frowning:': 'ðŸ™â€â™€ï¸', + ':woman_genie:': '🧞â€â™€ï¸', + ':woman_gesturing_no:': '🙅â€â™€ï¸', + ':woman_gesturing_ok:': '🙆â€â™€ï¸', + ':woman_getting_face_massage:': '💆â€â™€ï¸', + ':woman_getting_haircut:': '💇â€â™€ï¸', + ':woman_golfing:': 'ðŸŒï¸â€â™€ï¸', + ':woman_guard:': '💂â€â™€ï¸', + ':woman_health_worker:': '👩â€âš•ï¸', + ':woman_in_lotus_position:': '🧘â€â™€ï¸', + ':woman_in_steamy_room:': '🧖â€â™€ï¸', + ':woman_judge:': '👩â€âš–ï¸', + ':woman_juggling:': '🤹â€â™€ï¸', + ':woman_kneeling:': '🧎â€â™€ï¸', + ':woman_lifting_weights:': 'ðŸ‹ï¸â€â™€ï¸', + ':woman_mage:': '🧙â€â™€ï¸', + ':woman_mountain_biking:': '🚵â€â™€ï¸', + ':woman_pilot:': '👩â€âœˆï¸', + ':woman_playing_handball:': '🤾â€â™€ï¸', + ':woman_playing_water_polo:': '🤽â€â™€ï¸', + ':woman_police_officer:': '👮â€â™€ï¸', + ':woman_pouting:': '🙎â€â™€ï¸', + ':woman_raising_hand:': '🙋â€â™€ï¸', + ':woman_rowing_boat:': '🚣â€â™€ï¸', + ':woman_running:': 'ðŸƒâ€â™€ï¸', + ':woman_shrugging:': '🤷â€â™€ï¸', + ':woman_standing:': 'ðŸ§â€â™€ï¸', + ':woman_superhero:': '🦸â€â™€ï¸', + ':woman_supervillain:': '🦹â€â™€ï¸', + ':woman_surfing:': 'ðŸ„â€â™€ï¸', + ':woman_swimming:': 'ðŸŠâ€â™€ï¸', + ':woman_tipping_hand:': 'ðŸ’â€â™€ï¸', + ':woman_vampire:': '🧛â€â™€ï¸', + ':woman_walking:': '🚶â€â™€ï¸', + ':woman_wearing_turban:': '👳â€â™€ï¸', + ':woman_zombie:': '🧟â€â™€ï¸', + ':women_with_bunny_ears_partying:': '👯â€â™€ï¸', + ':women_wrestling:': '🤼♀ï¸', + ':writing_hand_tone1:': 'âœï¸ðŸ»', + ':writing_hand_tone2:': 'âœï¸ðŸ¼', + ':writing_hand_tone3:': 'âœï¸ðŸ½', + ':writing_hand_tone4:': 'âœï¸ðŸ¾', + ':writing_hand_tone5:': 'âœï¸ðŸ¿', + ':keycap_asterisk:': '*ï¸âƒ£', + ':asterisk:': '*ï¸âƒ£', + ':eight:': '8ï¸âƒ£', + ':five:': '5ï¸âƒ£', + ':four:': '4ï¸âƒ£', + ':hash:': '#ï¸âƒ£', + ':man_bouncing_ball:': '⛹ï¸â€â™‚ï¸', + ':nine:': '9ï¸âƒ£', + ':one:': '1ï¸âƒ£', + ':seven:': '7ï¸âƒ£', + ':six:': '6ï¸âƒ£', + ':three:': '3ï¸âƒ£', + ':two:': '2ï¸âƒ£', + ':woman_bouncing_ball:': '⛹ï¸â€â™€ï¸', + ':zero:': '0ï¸âƒ£', + ':100:': '💯', + ':1234:': '🔢', + ':8ball:': '🎱', + ':a:': '🅰ï¸', + ':ab:': '🆎', + ':abacus:': '🧮', + ':abc:': '🔤', + ':abcd:': '🔡', + ':accept:': '🉑', + ':adhesive_bandage:': '🩹', + ':adult:': '🧑', + ':aerial_tramway:': '🚡', + ':airplane_arriving:': '🛬', + ':airplane_departure:': '🛫', + ':small_airplane:': '🛩ï¸', + ':airplane_small:': '🛩ï¸', + ':alien:': '👽', + ':ambulance:': '🚑', + ':amphora:': 'ðŸº', + ':angel:': '👼', + ':anger:': '💢', + ':right_anger_bubble:': '🗯ï¸', + ':anger_right:': '🗯ï¸', + ':angry:': '😠', + ':anguished:': '😧', + ':ant:': 'ðŸœ', + ':apple:': 'ðŸŽ', + ':arrow_down_small:': '🔽', + ':arrow_up_small:': '🔼', + ':arrows_clockwise:': '🔃', + ':arrows_counterclockwise:': '🔄', + ':art:': '🎨', + ':articulated_lorry:': '🚛', + ':astonished:': '😲', + ':athletic_shoe:': '👟', + ':atm:': 'ðŸ§', + ':auto_rickshaw:': '🛺', + ':avocado:': '🥑', + ':axe:': '🪓', + ':b:': '🅱ï¸', + ':baby:': '👶', + ':baby_bottle:': 'ðŸ¼', + ':baby_chick:': 'ðŸ¤', + ':baby_symbol:': '🚼', + ':back:': '🔙', + ':bacon:': '🥓', + ':badger:': '🦡', + ':badminton:': 'ðŸ¸', + ':bagel:': '🥯', + ':baggage_claim:': '🛄', + ':bald:': '🦲', + ':ballet_shoes:': '🩰', + ':balloon:': '🎈', + ':ballot_box_with_ballot:': '🗳ï¸', + ':ballot_box:': '🗳ï¸', + ':bamboo:': 'ðŸŽ', + ':banana:': 'ðŸŒ', + ':banjo:': '🪕', + ':bank:': 'ðŸ¦', + ':bar_chart:': '📊', + ':barber:': '💈', + ':basket:': '🧺', + ':basketball:': 'ðŸ€', + ':bat:': '🦇', + ':bath:': '🛀', + ':bathtub:': 'ðŸ›', + ':battery:': '🔋', + ':beach_with_umbrella:': 'ðŸ–ï¸', + ':beach:': 'ðŸ–ï¸', + ':bear:': 'ðŸ»', + ':bearded_person:': '🧔', + ':bed:': 'ðŸ›ï¸', + ':bee:': 'ðŸ', + ':beer:': 'ðŸº', + ':beers:': 'ðŸ»', + ':beetle:': 'ðŸž', + ':beginner:': '🔰', + ':bell:': '🔔', + ':bellhop_bell:': '🛎ï¸', + ':bellhop:': '🛎ï¸', + ':bento:': 'ðŸ±', + ':beverage_box:': '🧃', + ':bike:': '🚲', + ':bikini:': '👙', + ':billed_cap:': '🧢', + ':bird:': 'ðŸ¦', + ':birthday:': '🎂', + ':black_heart:': '🖤', + ':black_joker:': 'ðŸƒ', + ':black_square_button:': '🔲', + ':person_with_blond_hair:': '👱', + ':blond_haired_person:': '👱', + ':blossom:': '🌼', + ':blowfish:': 'ðŸ¡', + ':blue_book:': '📘', + ':blue_car:': '🚙', + ':blue_circle:': '🔵', + ':blue_heart:': '💙', + ':blue_square:': '🟦', + ':blush:': '😊', + ':boar:': 'ðŸ—', + ':bomb:': '💣', + ':bone:': '🦴', + ':book:': '📖', + ':bookmark:': '🔖', + ':bookmark_tabs:': '📑', + ':books:': '📚', + ':boom:': '💥', + ':boot:': '👢', + ':bouquet:': 'ðŸ’', + ':archery:': 'ðŸ¹', + ':bow_and_arrow:': 'ðŸ¹', + ':bowl_with_spoon:': '🥣', + ':bowling:': '🎳', + ':boxing_gloves:': '🥊', + ':boxing_glove:': '🥊', + ':boy:': '👦', + ':brain:': '🧠', + ':bread:': 'ðŸž', + ':breast_feeding:': '🤱', + ':bricks:': '🧱', + ':bride_with_veil:': '👰', + ':bridge_at_night:': '🌉', + ':briefcase:': '💼', + ':briefs:': '🩲', + ':broccoli:': '🥦', + ':broken_heart:': '💔', + ':broom:': '🧹', + ':brown_circle:': '🟤', + ':brown_heart:': '🤎', + ':brown_square:': '🟫', + ':bug:': 'ðŸ›', + ':bulb:': '💡', + ':bullettrain_front:': '🚅', + ':bullettrain_side:': '🚄', + ':burrito:': '🌯', + ':bus:': '🚌', + ':busstop:': 'ðŸš', + ':bust_in_silhouette:': '👤', + ':busts_in_silhouette:': '👥', + ':butter:': '🧈', + ':butterfly:': '🦋', + ':cactus:': '🌵', + ':cake:': 'ðŸ°', + ':calendar:': '📆', + ':spiral_calendar_pad:': '🗓ï¸', + ':calendar_spiral:': '🗓ï¸', + ':call_me_hand:': '🤙', + ':call_me:': '🤙', + ':calling:': '📲', + ':camel:': 'ðŸ«', + ':camera:': '📷', + ':camera_with_flash:': '📸', + ':camping:': 'ðŸ•ï¸', + ':candle:': '🕯ï¸', + ':candy:': 'ðŸ¬', + ':canned_food:': '🥫', + ':kayak:': '🛶', + ':canoe:': '🛶', + ':capital_abcd:': '🔠', + ':card_file_box:': '🗃ï¸', + ':card_box:': '🗃ï¸', + ':card_index:': '📇', + ':carousel_horse:': '🎠', + ':carrot:': '🥕', + ':cat2:': 'ðŸˆ', + ':cat:': 'ðŸ±', + ':cd:': '💿', + ':chair:': '🪑', + ':bottle_with_popping_cork:': 'ðŸ¾', + ':champagne:': 'ðŸ¾', + ':clinking_glass:': '🥂', + ':champagne_glass:': '🥂', + ':chart:': '💹', + ':chart_with_downwards_trend:': '📉', + ':chart_with_upwards_trend:': '📈', + ':checkered_flag:': 'ðŸ', + ':cheese_wedge:': '🧀', + ':cheese:': '🧀', + ':cherries:': 'ðŸ’', + ':cherry_blossom:': '🌸', + ':chestnut:': '🌰', + ':chicken:': 'ðŸ”', + ':child:': '🧒', + ':children_crossing:': '🚸', + ':chipmunk:': 'ðŸ¿ï¸', + ':chocolate_bar:': 'ðŸ«', + ':chopsticks:': '🥢', + ':christmas_tree:': '🎄', + ':cinema:': '🎦', + ':circus_tent:': '🎪', + ':city_dusk:': '🌆', + ':city_sunrise:': '🌇', + ':city_sunset:': '🌇', + ':cityscape:': 'ðŸ™ï¸', + ':cl:': '🆑', + ':clap:': 'ðŸ‘', + ':clapper:': '🎬', + ':classical_building:': 'ðŸ›ï¸', + ':clipboard:': '📋', + ':clock1030:': '🕥', + ':clock10:': '🕙', + ':clock1130:': '🕦', + ':clock11:': '🕚', + ':clock1230:': '🕧', + ':clock12:': '🕛', + ':clock130:': '🕜', + ':clock1:': 'ðŸ•', + ':clock230:': 'ðŸ•', + ':clock2:': '🕑', + ':clock330:': '🕞', + ':clock3:': '🕒', + ':clock430:': '🕟', + ':clock4:': '🕓', + ':clock530:': '🕠', + ':clock5:': '🕔', + ':clock630:': '🕡', + ':clock6:': '🕕', + ':clock730:': '🕢', + ':clock7:': '🕖', + ':clock830:': '🕣', + ':clock8:': '🕗', + ':clock930:': '🕤', + ':clock9:': '🕘', + ':mantlepiece_clock:': '🕰ï¸', + ':clock:': '🕰ï¸', + ':closed_book:': '📕', + ':closed_lock_with_key:': 'ðŸ”', + ':closed_umbrella:': '🌂', + ':cloud_with_lightning:': '🌩ï¸', + ':cloud_lightning:': '🌩ï¸', + ':cloud_with_rain:': '🌧ï¸', + ':cloud_rain:': '🌧ï¸', + ':cloud_with_snow:': '🌨ï¸', + ':cloud_snow:': '🌨ï¸', + ':cloud_with_tornado:': '🌪ï¸', + ':cloud_tornado:': '🌪ï¸', + ':clown_face:': '🤡', + ':clown:': '🤡', + ':coat:': '🧥', + ':cocktail:': 'ðŸ¸', + ':coconut:': '🥥', + ':cold_face:': '🥶', + ':cold_sweat:': '😰', + ':compass:': 'ðŸ§', + ':compression:': '🗜ï¸', + ':computer:': '💻', + ':confetti_ball:': '🎊', + ':confounded:': '😖', + ':confused:': '😕', + ':construction:': '🚧', + ':building_construction:': 'ðŸ—ï¸', + ':construction_site:': 'ðŸ—ï¸', + ':construction_worker:': '👷', + ':control_knobs:': '🎛ï¸', + ':convenience_store:': 'ðŸª', + ':cookie:': 'ðŸª', + ':cooking:': 'ðŸ³', + ':cool:': '🆒', + ':corn:': '🌽', + ':couch_and_lamp:': '🛋ï¸', + ':couch:': '🛋ï¸', + ':couple:': '👫', + ':couple_with_heart:': '💑', + ':couplekiss:': 'ðŸ’', + ':cow2:': 'ðŸ„', + ':cow:': 'ðŸ®', + ':face_with_cowboy_hat:': '🤠', + ':cowboy:': '🤠', + ':crab:': '🦀', + ':lower_left_crayon:': 'ðŸ–ï¸', + ':crayon:': 'ðŸ–ï¸', + ':credit_card:': '💳', + ':crescent_moon:': '🌙', + ':cricket:': '🦗', + ':cricket_bat_ball:': 'ðŸ', + ':cricket_game:': 'ðŸ', + ':crocodile:': 'ðŸŠ', + ':croissant:': 'ðŸ¥', + ':crossed_flags:': '🎌', + ':crown:': '👑', + ':passenger_ship:': '🛳ï¸', + ':cruise_ship:': '🛳ï¸', + ':cry:': '😢', + ':crying_cat_face:': '😿', + ':crystal_ball:': '🔮', + ':cucumber:': '🥒', + ':cup_with_straw:': '🥤', + ':cupcake:': 'ðŸ§', + ':cupid:': '💘', + ':curling_stone:': '🥌', + ':curly_haired:': '🦱', + ':currency_exchange:': '💱', + ':curry:': 'ðŸ›', + ':pudding:': 'ðŸ®', + ':flan:': 'ðŸ®', + ':custard:': 'ðŸ®', + ':customs:': '🛃', + ':cut_of_meat:': '🥩', + ':cyclone:': '🌀', + ':dagger_knife:': '🗡ï¸', + ':dagger:': '🗡ï¸', + ':dancer:': '💃', + ':dango:': 'ðŸ¡', + ':dark_sunglasses:': '🕶ï¸', + ':dart:': '🎯', + ':dash:': '💨', + ':date:': '📅', + ':deaf_person:': 'ðŸ§', + ':deciduous_tree:': '🌳', + ':deer:': '🦌', + ':department_store:': 'ðŸ¬', + ':desert:': 'ðŸœï¸', + ':desktop_computer:': '🖥ï¸', + ':desktop:': '🖥ï¸', + ':spy:': '🕵ï¸', + ':sleuth_or_spy:': '🕵ï¸', + ':detective:': '🕵ï¸', + ':diamond_shape_with_a_dot_inside:': '💠', + ':disappointed:': '😞', + ':disappointed_relieved:': '😥', + ':card_index_dividers:': '🗂ï¸', + ':dividers:': '🗂ï¸', + ':diving_mask:': '🤿', + ':diya_lamp:': '🪔', + ':dizzy:': '💫', + ':dizzy_face:': '😵', + ':dna:': '🧬', + ':do_not_litter:': '🚯', + ':dog2:': 'ðŸ•', + ':dog:': 'ðŸ¶', + ':dollar:': '💵', + ':dolls:': '🎎', + ':dolphin:': 'ðŸ¬', + ':door:': '🚪', + ':doughnut:': 'ðŸ©', + ':dove_of_peace:': '🕊ï¸', + ':dove:': '🕊ï¸', + ':dragon:': 'ðŸ‰', + ':dragon_face:': 'ðŸ²', + ':dress:': '👗', + ':dromedary_camel:': 'ðŸª', + ':drool:': '🤤', + ':drooling_face:': '🤤', + ':drop_of_blood:': '🩸', + ':droplet:': '💧', + ':drum_with_drumsticks:': 'ðŸ¥', + ':drum:': 'ðŸ¥', + ':duck:': '🦆', + ':dumpling:': '🥟', + ':dvd:': '📀', + ':email:': '📧', + ':e-mail:': '📧', + ':eagle:': '🦅', + ':ear:': '👂', + ':ear_of_rice:': '🌾', + ':ear_with_hearing_aid:': '🦻', + ':earth_africa:': 'ðŸŒ', + ':earth_americas:': '🌎', + ':earth_asia:': 'ðŸŒ', + ':egg:': '🥚', + ':eggplant:': 'ðŸ†', + ':electric_plug:': '🔌', + ':elephant:': 'ðŸ˜', + ':elf:': 'ðŸ§', + ':end:': '🔚', + ':envelope_with_arrow:': '📩', + ':euro:': '💶', + ':european_castle:': 'ðŸ°', + ':european_post_office:': 'ðŸ¤', + ':evergreen_tree:': '🌲', + ':exploding_head:': '🤯', + ':expressionless:': '😑', + ':eye:': 'ðŸ‘ï¸', + ':eyeglasses:': '👓', + ':eyes:': '👀', + ':face_vomiting:': '🤮', + ':face_with_hand_over_mouth:': 'ðŸ¤', + ':face_with_monocle:': 'ðŸ§', + ':face_with_raised_eyebrow:': '🤨', + ':face_with_symbols_over_mouth:': '🤬', + ':factory:': 'ðŸ', + ':fairy:': '🧚', + ':falafel:': '🧆', + ':fallen_leaf:': 'ðŸ‚', + ':family:': '👪', + ':fax:': '📠', + ':fearful:': '😨', + ':paw_prints:': 'ðŸ¾', + ':feet:': 'ðŸ¾', + ':ferris_wheel:': '🎡', + ':field_hockey:': 'ðŸ‘', + ':file_cabinet:': '🗄ï¸', + ':file_folder:': 'ðŸ“', + ':film_frames:': '🎞ï¸', + ':hand_with_index_and_middle_finger_crossed:': '🤞', + ':fingers_crossed:': '🤞', + ':flame:': '🔥', + ':fire:': '🔥', + ':fire_engine:': '🚒', + ':fire_extinguisher:': '🧯', + ':firecracker:': '🧨', + ':fireworks:': '🎆', + ':first_place_medal:': '🥇', + ':first_place:': '🥇', + ':first_quarter_moon:': '🌓', + ':first_quarter_moon_with_face:': '🌛', + ':fish:': 'ðŸŸ', + ':fish_cake:': 'ðŸ¥', + ':fishing_pole_and_fish:': '🎣', + ':waving_black_flag:': 'ðŸ´', + ':flag_black:': 'ðŸ´', + ':waving_white_flag:': 'ðŸ³ï¸', + ':flag_white:': 'ðŸ³ï¸', + ':flags:': 'ðŸŽ', + ':flamingo:': '🦩', + ':flashlight:': '🔦', + ':floppy_disk:': '💾', + ':flower_playing_cards:': '🎴', + ':flushed:': '😳', + ':flying_disc:': 'ðŸ¥', + ':flying_saucer:': '🛸', + ':fog:': '🌫ï¸', + ':foggy:': 'ðŸŒ', + ':foot:': '🦶', + ':football:': 'ðŸˆ', + ':footprints:': '👣', + ':fork_and_knife:': 'ðŸ´', + ':fork_and_knife_with_plate:': 'ðŸ½ï¸', + ':fork_knife_plate:': 'ðŸ½ï¸', + ':fortune_cookie:': '🥠', + ':four_leaf_clover:': 'ðŸ€', + ':fox_face:': '🦊', + ':fox:': '🦊', + ':frame_with_picture:': '🖼ï¸', + ':frame_photo:': '🖼ï¸', + ':free:': '🆓', + ':baguette_bread:': '🥖', + ':french_bread:': '🥖', + ':fried_shrimp:': 'ðŸ¤', + ':fries:': 'ðŸŸ', + ':frog:': 'ðŸ¸', + ':frowning:': '😦', + ':full_moon:': '🌕', + ':full_moon_with_face:': 'ðŸŒ', + ':game_die:': '🎲', + ':garlic:': '🧄', + ':gem:': '💎', + ':genie:': '🧞', + ':ghost:': '👻', + ':gift:': 'ðŸŽ', + ':gift_heart:': 'ðŸ’', + ':giraffe:': '🦒', + ':girl:': '👧', + ':globe_with_meridians:': 'ðŸŒ', + ':gloves:': '🧤', + ':goal_net:': '🥅', + ':goal:': '🥅', + ':goat:': 'ðŸ', + ':goggles:': '🥽', + ':gorilla:': 'ðŸ¦', + ':grapes:': 'ðŸ‡', + ':green_apple:': 'ðŸ', + ':green_book:': '📗', + ':green_circle:': '🟢', + ':green_heart:': '💚', + ':green_square:': '🟩', + ':grimacing:': '😬', + ':grin:': 'ðŸ˜', + ':grinning:': '😀', + ':guardsman:': '💂', + ':guard:': '💂', + ':guide_dog:': '🦮', + ':guitar:': '🎸', + ':gun:': '🔫', + ':hamburger:': 'ðŸ”', + ':hammer:': '🔨', + ':hamster:': 'ðŸ¹', + ':raised_hand_with_fingers_splayed:': 'ðŸ–ï¸', + ':hand_splayed:': 'ðŸ–ï¸', + ':handbag:': '👜', + ':shaking_hands:': 'ðŸ¤', + ':handshake:': 'ðŸ¤', + ':hatched_chick:': 'ðŸ¥', + ':hatching_chick:': 'ðŸ£', + ':face_with_head_bandage:': '🤕', + ':head_bandage:': '🤕', + ':headphones:': '🎧', + ':hear_no_evil:': '🙉', + ':heart_decoration:': '💟', + ':heart_eyes:': 'ðŸ˜', + ':heart_eyes_cat:': '😻', + ':heartbeat:': '💓', + ':heartpulse:': '💗', + ':heavy_dollar_sign:': '💲', + ':hedgehog:': '🦔', + ':helicopter:': 'ðŸš', + ':herb:': '🌿', + ':hibiscus:': '🌺', + ':high_brightness:': '🔆', + ':high_heel:': '👠', + ':hiking_boot:': '🥾', + ':hindu_temple:': '🛕', + ':hippopotamus:': '🦛', + ':hockey:': 'ðŸ’', + ':hole:': '🕳ï¸', + ':house_buildings:': 'ðŸ˜ï¸', + ':homes:': 'ðŸ˜ï¸', + ':honey_pot:': 'ðŸ¯', + ':horse:': 'ðŸ´', + ':horse_racing:': 'ðŸ‡', + ':hospital:': 'ðŸ¥', + ':hot_face:': '🥵', + ':hot_pepper:': '🌶ï¸', + ':hot_dog:': 'ðŸŒ', + ':hotdog:': 'ðŸŒ', + ':hotel:': 'ðŸ¨', + ':house:': 'ðŸ ', + ':derelict_house_building:': 'ðŸšï¸', + ':house_abandoned:': 'ðŸšï¸', + ':house_with_garden:': 'ðŸ¡', + ':hugging_face:': '🤗', + ':hugging:': '🤗', + ':hushed:': '😯', + ':ice_cream:': 'ðŸ¨', + ':ice_cube:': '🧊', + ':icecream:': 'ðŸ¦', + ':id:': '🆔', + ':ideograph_advantage:': 'ðŸ‰', + ':imp:': '👿', + ':inbox_tray:': '📥', + ':incoming_envelope:': '📨', + ':innocent:': '😇', + ':iphone:': '📱', + ':desert_island:': 'ðŸï¸', + ':island:': 'ðŸï¸', + ':izakaya_lantern:': 'ðŸ®', + ':jack_o_lantern:': '🎃', + ':japan:': '🗾', + ':japanese_castle:': 'ðŸ¯', + ':japanese_goblin:': '👺', + ':japanese_ogre:': '👹', + ':jeans:': '👖', + ':jigsaw:': '🧩', + ':joy:': '😂', + ':joy_cat:': '😹', + ':joystick:': '🕹ï¸', + ':kaaba:': '🕋', + ':kangaroo:': '🦘', + ':old_key:': 'ðŸ—ï¸', + ':key2:': 'ðŸ—ï¸', + ':key:': '🔑', + ':keycap_ten:': '🔟', + ':kimono:': '👘', + ':kiss:': '💋', + ':kissing:': '😗', + ':kissing_cat:': '😽', + ':kissing_closed_eyes:': '😚', + ':kissing_heart:': '😘', + ':kissing_smiling_eyes:': '😙', + ':kite:': 'ðŸª', + ':kiwifruit:': 'ðŸ¥', + ':kiwi:': 'ðŸ¥', + ':knife:': '🔪', + ':koala:': 'ðŸ¨', + ':koko:': 'ðŸˆ', + ':lab_coat:': '🥼', + ':label:': 'ðŸ·ï¸', + ':lacrosse:': 'ðŸ¥', + ':large_blue_diamond:': '🔷', + ':large_orange_diamond:': '🔶', + ':last_quarter_moon:': '🌗', + ':last_quarter_moon_with_face:': '🌜', + ':satisfied:': '😆', + ':laughing:': '😆', + ':leafy_green:': '🥬', + ':leaves:': 'ðŸƒ', + ':ledger:': '📒', + ':left_fist:': '🤛', + ':left_facing_fist:': '🤛', + ':left_luggage:': '🛅', + ':leg:': '🦵', + ':lemon:': 'ðŸ‹', + ':leopard:': 'ðŸ†', + ':level_slider:': '🎚ï¸', + ':man_in_business_suit_levitating:': '🕴ï¸', + ':levitate:': '🕴ï¸', + ':light_rail:': '🚈', + ':link:': '🔗', + ':lion:': 'ðŸ¦', + ':lion_face:': 'ðŸ¦', + ':lips:': '👄', + ':lipstick:': '💄', + ':lizard:': '🦎', + ':llama:': '🦙', + ':lobster:': '🦞', + ':lock:': '🔒', + ':lock_with_ink_pen:': 'ðŸ”', + ':lollipop:': 'ðŸ', + ':loud_sound:': '🔊', + ':loudspeaker:': '📢', + ':love_hotel:': 'ðŸ©', + ':love_letter:': '💌', + ':love_you_gesture:': '🤟', + ':low_brightness:': '🔅', + ':luggage:': '🧳', + ':liar:': '🤥', + ':lying_face:': '🤥', + ':mag:': 'ðŸ”', + ':mag_right:': '🔎', + ':mage:': '🧙', + ':magnet:': '🧲', + ':mahjong:': '🀄', + ':mailbox:': '📫', + ':mailbox_closed:': '📪', + ':mailbox_with_mail:': '📬', + ':mailbox_with_no_mail:': 'ðŸ“', + ':man:': '👨', + ':male_dancer:': '🕺', + ':man_dancing:': '🕺', + ':man_in_tuxedo:': '🤵', + ':man_with_gua_pi_mao:': '👲', + ':man_with_chinese_cap:': '👲', + ':mango:': 'ðŸ¥', + ':mans_shoe:': '👞', + ':manual_wheelchair:': '🦽', + ':world_map:': '🗺ï¸', + ':map:': '🗺ï¸', + ':maple_leaf:': 'ðŸ', + ':karate_uniform:': '🥋', + ':martial_arts_uniform:': '🥋', + ':mask:': '😷', + ':mate:': '🧉', + ':meat_on_bone:': 'ðŸ–', + ':mechanical_arm:': '🦾', + ':mechanical_leg:': '🦿', + ':sports_medal:': 'ðŸ…', + ':medal:': 'ðŸ…', + ':mega:': '📣', + ':melon:': 'ðŸˆ', + ':menorah:': '🕎', + ':mens:': '🚹', + ':merperson:': '🧜', + ':sign_of_the_horns:': '🤘', + ':metal:': '🤘', + ':metro:': '🚇', + ':microbe:': '🦠', + ':studio_microphone:': '🎙ï¸', + ':microphone2:': '🎙ï¸', + ':microphone:': '🎤', + ':microscope:': '🔬', + ':reversed_hand_with_middle_finger_extended:': '🖕', + ':middle_finger:': '🖕', + ':military_medal:': '🎖ï¸', + ':glass_of_milk:': '🥛', + ':milk:': '🥛', + ':milky_way:': '🌌', + ':minibus:': 'ðŸš', + ':minidisc:': '💽', + ':mobile_phone_off:': '📴', + ':money_mouth_face:': '🤑', + ':money_mouth:': '🤑', + ':money_with_wings:': '💸', + ':moneybag:': '💰', + ':monkey:': 'ðŸ’', + ':monkey_face:': 'ðŸµ', + ':monorail:': 'ðŸš', + ':moon_cake:': '🥮', + ':mortar_board:': '🎓', + ':mosque:': '🕌', + ':mosquito:': '🦟', + ':motorbike:': '🛵', + ':motor_scooter:': '🛵', + ':motorboat:': '🛥ï¸', + ':racing_motorcycle:': 'ðŸï¸', + ':motorcycle:': 'ðŸï¸', + ':motorized_wheelchair:': '🦼', + ':motorway:': '🛣ï¸', + ':mount_fuji:': '🗻', + ':mountain_cableway:': '🚠', + ':mountain_railway:': '🚞', + ':snow_capped_mountain:': 'ðŸ”ï¸', + ':mountain_snow:': 'ðŸ”ï¸', + ':mouse2:': 'ðŸ', + ':mouse:': 'ðŸ', + ':three_button_mouse:': '🖱ï¸', + ':mouse_three_button:': '🖱ï¸', + ':movie_camera:': '🎥', + ':moyai:': '🗿', + ':mother_christmas:': '🤶', + ':mrs_claus:': '🤶', + ':muscle:': '💪', + ':mushroom:': 'ðŸ„', + ':musical_keyboard:': '🎹', + ':musical_note:': '🎵', + ':musical_score:': '🎼', + ':mute:': '🔇', + ':nail_care:': '💅', + ':name_badge:': '📛', + ':sick:': '🤢', + ':nauseated_face:': '🤢', + ':nazar_amulet:': '🧿', + ':necktie:': '👔', + ':nerd_face:': '🤓', + ':nerd:': '🤓', + ':neutral_face:': 'ðŸ˜', + ':new:': '🆕', + ':new_moon:': '🌑', + ':new_moon_with_face:': '🌚', + ':rolled_up_newspaper:': '🗞ï¸', + ':newspaper2:': '🗞ï¸', + ':newspaper:': '📰', + ':ng:': '🆖', + ':night_with_stars:': '🌃', + ':no_bell:': '🔕', + ':no_bicycles:': '🚳', + ':no_entry_sign:': '🚫', + ':no_mobile_phones:': '📵', + ':no_mouth:': '😶', + ':no_pedestrians:': '🚷', + ':no_smoking:': 'ðŸš', + ':non-potable_water:': '🚱', + ':nose:': '👃', + ':notebook:': '📓', + ':notebook_with_decorative_cover:': '📔', + ':spiral_note_pad:': '🗒ï¸', + ':notepad_spiral:': '🗒ï¸', + ':notes:': '🎶', + ':nut_and_bolt:': '🔩', + ':o2:': '🅾ï¸', + ':ocean:': '🌊', + ':stop_sign:': '🛑', + ':octagonal_sign:': '🛑', + ':octopus:': 'ðŸ™', + ':oden:': 'ðŸ¢', + ':office:': 'ðŸ¢', + ':oil_drum:': '🛢ï¸', + ':oil:': '🛢ï¸', + ':ok:': '🆗', + ':ok_hand:': '👌', + ':older_adult:': '🧓', + ':older_man:': '👴', + ':grandma:': '👵', + ':older_woman:': '👵', + ':om_symbol:': '🕉ï¸', + ':on:': '🔛', + ':oncoming_automobile:': '🚘', + ':oncoming_bus:': 'ðŸš', + ':oncoming_police_car:': '🚔', + ':oncoming_taxi:': '🚖', + ':one_piece_swimsuit:': '🩱', + ':onion:': '🧅', + ':open_file_folder:': '📂', + ':open_hands:': 'ðŸ‘', + ':open_mouth:': '😮', + ':orange_book:': '📙', + ':orange_circle:': '🟠', + ':orange_heart:': '🧡', + ':orange_square:': '🟧', + ':orangutan:': '🦧', + ':otter:': '🦦', + ':outbox_tray:': '📤', + ':owl:': '🦉', + ':ox:': 'ðŸ‚', + ':oyster:': '🦪', + ':package:': '📦', + ':page_facing_up:': '📄', + ':page_with_curl:': '📃', + ':pager:': '📟', + ':lower_left_paintbrush:': '🖌ï¸', + ':paintbrush:': '🖌ï¸', + ':palm_tree:': '🌴', + ':palms_up_together:': '🤲', + ':pancakes:': '🥞', + ':panda_face:': 'ðŸ¼', + ':paperclip:': '📎', + ':linked_paperclips:': '🖇ï¸', + ':paperclips:': '🖇ï¸', + ':parachute:': '🪂', + ':national_park:': 'ðŸžï¸', + ':park:': 'ðŸžï¸', + ':parking:': '🅿ï¸', + ':parrot:': '🦜', + ':partying_face:': '🥳', + ':passport_control:': '🛂', + ':peach:': 'ðŸ‘', + ':peacock:': '🦚', + ':shelled_peanut:': '🥜', + ':peanuts:': '🥜', + ':pear:': 'ðŸ', + ':lower_left_ballpoint_pen:': '🖊ï¸', + ':pen_ballpoint:': '🖊ï¸', + ':lower_left_fountain_pen:': '🖋ï¸', + ':pen_fountain:': '🖋ï¸', + ':memo:': 'ðŸ“', + ':pencil:': 'ðŸ“', + ':penguin:': 'ðŸ§', + ':pensive:': '😔', + ':dancers:': '👯', + ':people_with_bunny_ears_partying:': '👯', + ':wrestlers:': '🤼', + ':wrestling:': '🤼', + ':people_wrestling:': '🤼', + ':performing_arts:': 'ðŸŽ', + ':persevere:': '😣', + ':bicyclist:': '🚴', + ':person_biking:': '🚴', + ':bow:': '🙇', + ':person_bowing:': '🙇', + ':person_climbing:': '🧗', + ':cartwheel:': '🤸', + ':person_doing_cartwheel:': '🤸', + ':face_palm:': '🤦', + ':facepalm:': '🤦', + ':person_facepalming:': '🤦', + ':fencer:': '🤺', + ':fencing:': '🤺', + ':person_fencing:': '🤺', + ':person_frowning:': 'ðŸ™', + ':no_good:': '🙅', + ':person_gesturing_no:': '🙅', + ':ok_woman:': '🙆', + ':person_gesturing_ok:': '🙆', + ':haircut:': '💇', + ':person_getting_haircut:': '💇', + ':massage:': '💆', + ':person_getting_massage:': '💆', + ':golfer:': 'ðŸŒï¸', + ':person_golfing:': 'ðŸŒï¸', + ':person_in_lotus_position:': '🧘', + ':person_in_steamy_room:': '🧖', + ':juggling:': '🤹', + ':juggler:': '🤹', + ':person_juggling:': '🤹', + ':person_kneeling:': '🧎', + ':lifter:': 'ðŸ‹ï¸', + ':weight_lifter:': 'ðŸ‹ï¸', + ':person_lifting_weights:': 'ðŸ‹ï¸', + ':mountain_bicyclist:': '🚵', + ':person_mountain_biking:': '🚵', + ':handball:': '🤾', + ':person_playing_handball:': '🤾', + ':water_polo:': '🤽', + ':person_playing_water_polo:': '🤽', + ':person_with_pouting_face:': '🙎', + ':person_pouting:': '🙎', + ':raising_hand:': '🙋', + ':person_raising_hand:': '🙋', + ':rowboat:': '🚣', + ':person_rowing_boat:': '🚣', + ':runner:': 'ðŸƒ', + ':person_running:': 'ðŸƒ', + ':shrug:': '🤷', + ':person_shrugging:': '🤷', + ':person_standing:': 'ðŸ§', + ':surfer:': 'ðŸ„', + ':person_surfing:': 'ðŸ„', + ':swimmer:': 'ðŸŠ', + ':person_swimming:': 'ðŸŠ', + ':information_desk_person:': 'ðŸ’', + ':person_tipping_hand:': 'ðŸ’', + ':walking:': '🚶', + ':person_walking:': '🚶', + ':man_with_turban:': '👳', + ':person_wearing_turban:': '👳', + ':petri_dish:': '🧫', + ':pie:': '🥧', + ':pig2:': 'ðŸ–', + ':pig:': 'ðŸ·', + ':pig_nose:': 'ðŸ½', + ':pill:': '💊', + ':pinching_hand:': 'ðŸ¤', + ':pineapple:': 'ðŸ', + ':table_tennis:': 'ðŸ“', + ':ping_pong:': 'ðŸ“', + ':pizza:': 'ðŸ•', + ':worship_symbol:': 'ðŸ›', + ':place_of_worship:': 'ðŸ›', + ':pleading_face:': '🥺', + ':point_down:': '👇', + ':point_left:': '👈', + ':point_right:': '👉', + ':point_up_2:': '👆', + ':police_car:': '🚓', + ':cop:': '👮', + ':police_officer:': '👮', + ':poodle:': 'ðŸ©', + ':shit:': '💩', + ':hankey:': '💩', + ':poo:': '💩', + ':poop:': '💩', + ':popcorn:': 'ðŸ¿', + ':post_office:': 'ðŸ£', + ':postal_horn:': '📯', + ':postbox:': '📮', + ':potable_water:': '🚰', + ':potato:': '🥔', + ':pouch:': 'ðŸ‘', + ':poultry_leg:': 'ðŸ—', + ':pound:': '💷', + ':pouting_cat:': '😾', + ':pray:': 'ðŸ™', + ':prayer_beads:': '📿', + ':expecting_woman:': '🤰', + ':pregnant_woman:': '🤰', + ':pretzel:': '🥨', + ':prince:': '🤴', + ':princess:': '👸', + ':printer:': '🖨ï¸', + ':probing_cane:': '🦯', + ':film_projector:': '📽ï¸', + ':projector:': '📽ï¸', + ':punch:': '👊', + ':purple_circle:': '🟣', + ':purple_heart:': '💜', + ':purple_square:': '🟪', + ':purse:': '👛', + ':pushpin:': '📌', + ':put_litter_in_its_place:': '🚮', + ':rabbit2:': 'ðŸ‡', + ':rabbit:': 'ðŸ°', + ':raccoon:': 'ðŸ¦', + ':racing_car:': 'ðŸŽï¸', + ':race_car:': 'ðŸŽï¸', + ':racehorse:': 'ðŸŽ', + ':radio:': '📻', + ':radio_button:': '🔘', + ':rage:': '😡', + ':railway_car:': '🚃', + ':railroad_track:': '🛤ï¸', + ':railway_track:': '🛤ï¸', + ':rainbow:': '🌈', + ':back_of_hand:': '🤚', + ':raised_back_of_hand:': '🤚', + ':raised_hands:': '🙌', + ':ram:': 'ðŸ', + ':ramen:': 'ðŸœ', + ':rat:': 'ðŸ€', + ':razor:': '🪒', + ':receipt:': '🧾', + ':red_car:': '🚗', + ':red_circle:': '🔴', + ':red_envelope:': '🧧', + ':red_haired:': '🦰', + ':red_square:': '🟥', + ':regional_indicator_a:': '🇦', + ':regional_indicator_b:': '🇧', + ':regional_indicator_c:': '🇨', + ':regional_indicator_d:': '🇩', + ':regional_indicator_e:': '🇪', + ':regional_indicator_f:': '🇫', + ':regional_indicator_g:': '🇬', + ':regional_indicator_h:': 'ðŸ‡', + ':regional_indicator_i:': '🇮', + ':regional_indicator_j:': '🇯', + ':regional_indicator_k:': '🇰', + ':regional_indicator_l:': '🇱', + ':regional_indicator_m:': '🇲', + ':regional_indicator_n:': '🇳', + ':regional_indicator_o:': '🇴', + ':regional_indicator_p:': '🇵', + ':regional_indicator_q:': '🇶', + ':regional_indicator_r:': '🇷', + ':regional_indicator_s:': '🇸', + ':regional_indicator_t:': '🇹', + ':regional_indicator_u:': '🇺', + ':regional_indicator_v:': '🇻', + ':regional_indicator_w:': '🇼', + ':regional_indicator_x:': '🇽', + ':regional_indicator_y:': '🇾', + ':regional_indicator_z:': '🇿', + ':relieved:': '😌', + ':reminder_ribbon:': '🎗ï¸', + ':repeat:': 'ðŸ”', + ':repeat_one:': '🔂', + ':restroom:': '🚻', + ':revolving_hearts:': '💞', + ':rhinoceros:': 'ðŸ¦', + ':rhino:': 'ðŸ¦', + ':ribbon:': '🎀', + ':rice:': 'ðŸš', + ':rice_ball:': 'ðŸ™', + ':rice_cracker:': 'ðŸ˜', + ':rice_scene:': '🎑', + ':right_fist:': '🤜', + ':right_facing_fist:': '🤜', + ':ring:': 'ðŸ’', + ':ringed_planet:': 'ðŸª', + ':robot_face:': '🤖', + ':robot:': '🤖', + ':rocket:': '🚀', + ':rolling_on_the_floor_laughing:': '🤣', + ':rofl:': '🤣', + ':roll_of_paper:': '🧻', + ':roller_coaster:': '🎢', + ':face_with_rolling_eyes:': '🙄', + ':rolling_eyes:': '🙄', + ':rooster:': 'ðŸ“', + ':rose:': '🌹', + ':rosette:': 'ðŸµï¸', + ':rotating_light:': '🚨', + ':round_pushpin:': 'ðŸ“', + ':rugby_football:': 'ðŸ‰', + ':running_shirt_with_sash:': '🎽', + ':sa:': '🈂ï¸', + ':safety_pin:': '🧷', + ':safety_vest:': '🦺', + ':sake:': 'ðŸ¶', + ':green_salad:': '🥗', + ':salad:': '🥗', + ':salt:': '🧂', + ':sandal:': '👡', + ':sandwich:': '🥪', + ':santa:': '🎅', + ':sari:': '🥻', + ':satellite:': '📡', + ':satellite_orbital:': '🛰ï¸', + ':sauropod:': '🦕', + ':saxophone:': '🎷', + ':scarf:': '🧣', + ':school:': 'ðŸ«', + ':school_satchel:': '🎒', + ':scooter:': '🛴', + ':scorpion:': '🦂', + ':scream:': '😱', + ':scream_cat:': '🙀', + ':scroll:': '📜', + ':seat:': '💺', + ':second_place_medal:': '🥈', + ':second_place:': '🥈', + ':see_no_evil:': '🙈', + ':seedling:': '🌱', + ':selfie:': '🤳', + ':paella:': '🥘', + ':shallow_pan_of_food:': '🥘', + ':shark:': '🦈', + ':shaved_ice:': 'ðŸ§', + ':sheep:': 'ðŸ‘', + ':shell:': 'ðŸš', + ':shield:': '🛡ï¸', + ':ship:': '🚢', + ':shirt:': '👕', + ':shopping_bags:': 'ðŸ›ï¸', + ':shopping_trolley:': '🛒', + ':shopping_cart:': '🛒', + ':shorts:': '🩳', + ':shower:': '🚿', + ':shrimp:': 'ðŸ¦', + ':shushing_face:': '🤫', + ':signal_strength:': '📶', + ':six_pointed_star:': '🔯', + ':skateboard:': '🛹', + ':ski:': '🎿', + ':skeleton:': '💀', + ':skull:': '💀', + ':skunk:': '🦨', + ':sled:': '🛷', + ':sleeping:': '😴', + ':sleeping_accommodation:': '🛌', + ':sleepy:': '😪', + ':slightly_frowning_face:': 'ðŸ™', + ':slight_frown:': 'ðŸ™', + ':slightly_smiling_face:': '🙂', + ':slight_smile:': '🙂', + ':slot_machine:': '🎰', + ':sloth:': '🦥', + ':small_blue_diamond:': '🔹', + ':small_orange_diamond:': '🔸', + ':small_red_triangle:': '🔺', + ':small_red_triangle_down:': '🔻', + ':smile:': '😄', + ':smile_cat:': '😸', + ':smiley:': '😃', + ':smiley_cat:': '😺', + ':smiling_face_with_3_hearts:': '🥰', + ':smiling_imp:': '😈', + ':smirk:': 'ðŸ˜', + ':smirk_cat:': '😼', + ':smoking:': '🚬', + ':snail:': 'ðŸŒ', + ':snake:': 'ðŸ', + ':sneeze:': '🤧', + ':sneezing_face:': '🤧', + ':snowboarder:': 'ðŸ‚', + ':soap:': '🧼', + ':sob:': 'ðŸ˜', + ':socks:': '🧦', + ':softball:': '🥎', + ':soon:': '🔜', + ':sos:': '🆘', + ':sound:': '🔉', + ':space_invader:': '👾', + ':spaghetti:': 'ðŸ', + ':sparkler:': '🎇', + ':sparkling_heart:': '💖', + ':speak_no_evil:': '🙊', + ':speaker:': '🔈', + ':speaking_head_in_silhouette:': '🗣ï¸', + ':speaking_head:': '🗣ï¸', + ':speech_balloon:': '💬', + ':left_speech_bubble:': '🗨ï¸', + ':speech_left:': '🗨ï¸', + ':speedboat:': '🚤', + ':spider:': '🕷ï¸', + ':spider_web:': '🕸ï¸', + ':sponge:': '🧽', + ':spoon:': '🥄', + ':squeeze_bottle:': '🧴', + ':squid:': '🦑', + ':stadium:': 'ðŸŸï¸', + ':star2:': '🌟', + ':star_struck:': '🤩', + ':stars:': '🌠', + ':station:': '🚉', + ':statue_of_liberty:': '🗽', + ':steam_locomotive:': '🚂', + ':stethoscope:': '🩺', + ':stew:': 'ðŸ²', + ':straight_ruler:': 'ðŸ“', + ':strawberry:': 'ðŸ“', + ':stuck_out_tongue:': '😛', + ':stuck_out_tongue_closed_eyes:': 'ðŸ˜', + ':stuck_out_tongue_winking_eye:': '😜', + ':stuffed_pita:': '🥙', + ':stuffed_flatbread:': '🥙', + ':sun_with_face:': '🌞', + ':sunflower:': '🌻', + ':sunglasses:': '😎', + ':sunrise:': '🌅', + ':sunrise_over_mountains:': '🌄', + ':superhero:': '🦸', + ':supervillain:': '🦹', + ':sushi:': 'ðŸ£', + ':suspension_railway:': '🚟', + ':swan:': '🦢', + ':sweat:': '😓', + ':sweat_drops:': '💦', + ':sweat_smile:': '😅', + ':sweet_potato:': 'ðŸ ', + ':symbols:': '🔣', + ':synagogue:': 'ðŸ•', + ':syringe:': '💉', + ':t_rex:': '🦖', + ':taco:': '🌮', + ':tada:': '🎉', + ':takeout_box:': '🥡', + ':tanabata_tree:': '🎋', + ':tangerine:': 'ðŸŠ', + ':taxi:': '🚕', + ':tea:': 'ðŸµ', + ':teddy_bear:': '🧸', + ':telephone_receiver:': '📞', + ':telescope:': 'ðŸ”', + ':tennis:': '🎾', + ':test_tube:': '🧪', + ':thermometer:': '🌡ï¸', + ':face_with_thermometer:': '🤒', + ':thermometer_face:': '🤒', + ':thinking_face:': '🤔', + ':thinking:': '🤔', + ':third_place_medal:': '🥉', + ':third_place:': '🥉', + ':thought_balloon:': 'ðŸ’', + ':thread:': '🧵', + ':-1:': '👎', + ':thumbdown:': '👎', + ':thumbsdown:': '👎', + ':+1:': 'ðŸ‘', + ':thumbup:': 'ðŸ‘', + ':thumbsup:': 'ðŸ‘', + ':ticket:': '🎫', + ':admission_tickets:': '🎟ï¸', + ':tickets:': '🎟ï¸', + ':tiger2:': 'ðŸ…', + ':tiger:': 'ðŸ¯', + ':tired_face:': '😫', + ':toilet:': '🚽', + ':tokyo_tower:': '🗼', + ':tomato:': 'ðŸ…', + ':tone1:': 'ðŸ»', + ':tone2:': 'ðŸ¼', + ':tone3:': 'ðŸ½', + ':tone4:': 'ðŸ¾', + ':tone5:': 'ðŸ¿', + ':tongue:': '👅', + ':toolbox:': '🧰', + ':hammer_and_wrench:': '🛠ï¸', + ':tools:': '🛠ï¸', + ':tooth:': '🦷', + ':top:': 'ðŸ”', + ':tophat:': '🎩', + ':trackball:': '🖲ï¸', + ':tractor:': '🚜', + ':traffic_light:': '🚥', + ':train2:': '🚆', + ':train:': '🚋', + ':tram:': '🚊', + ':triangular_flag_on_post:': '🚩', + ':triangular_ruler:': 'ðŸ“', + ':trident:': '🔱', + ':triumph:': '😤', + ':trolleybus:': '🚎', + ':trophy:': 'ðŸ†', + ':tropical_drink:': 'ðŸ¹', + ':tropical_fish:': 'ðŸ ', + ':truck:': '🚚', + ':trumpet:': '🎺', + ':tulip:': '🌷', + ':whisky:': '🥃', + ':tumbler_glass:': '🥃', + ':turkey:': '🦃', + ':turtle:': 'ðŸ¢', + ':tv:': '📺', + ':twisted_rightwards_arrows:': '🔀', + ':two_hearts:': '💕', + ':two_men_holding_hands:': '👬', + ':two_women_holding_hands:': 'ðŸ‘', + ':u5272:': '🈹', + ':u5408:': '🈴', + ':u55b6:': '🈺', + ':u6307:': '🈯', + ':u6708:': '🈷ï¸', + ':u6709:': '🈶', + ':u6e80:': '🈵', + ':u7121:': '🈚', + ':u7533:': '🈸', + ':u7981:': '🈲', + ':u7a7a:': '🈳', + ':unamused:': '😒', + ':underage:': '🔞', + ':unicorn_face:': '🦄', + ':unicorn:': '🦄', + ':unlock:': '🔓', + ':up:': '🆙', + ':upside_down_face:': '🙃', + ':upside_down:': '🙃', + ':vampire:': '🧛', + ':vertical_traffic_light:': '🚦', + ':vhs:': '📼', + ':vibration_mode:': '📳', + ':video_camera:': '📹', + ':video_game:': '🎮', + ':violin:': '🎻', + ':volcano:': '🌋', + ':volleyball:': 'ðŸ', + ':vs:': '🆚', + ':raised_hand_with_part_between_middle_and_ring_fingers:': '🖖', + ':vulcan:': '🖖', + ':waffle:': '🧇', + ':waning_crescent_moon:': '🌘', + ':waning_gibbous_moon:': '🌖', + ':wastebasket:': '🗑ï¸', + ':water_buffalo:': 'ðŸƒ', + ':watermelon:': 'ðŸ‰', + ':wave:': '👋', + ':waxing_crescent_moon:': '🌒', + ':waxing_gibbous_moon:': '🌔', + ':wc:': '🚾', + ':weary:': '😩', + ':wedding:': '💒', + ':whale2:': 'ðŸ‹', + ':whale:': 'ðŸ³', + ':white_flower:': '💮', + ':white_haired:': '🦳', + ':white_heart:': 'ðŸ¤', + ':white_square_button:': '🔳', + ':white_sun_behind_cloud:': '🌥ï¸', + ':white_sun_cloud:': '🌥ï¸', + ':white_sun_behind_cloud_with_rain:': '🌦ï¸', + ':white_sun_rain_cloud:': '🌦ï¸', + ':white_sun_with_small_cloud:': '🌤ï¸', + ':white_sun_small_cloud:': '🌤ï¸', + ':wilted_flower:': '🥀', + ':wilted_rose:': '🥀', + ':wind_blowing_face:': '🌬ï¸', + ':wind_chime:': 'ðŸŽ', + ':wine_glass:': 'ðŸ·', + ':wink:': '😉', + ':wolf:': 'ðŸº', + ':woman:': '👩', + ':woman_with_headscarf:': '🧕', + ':womans_clothes:': '👚', + ':womans_flat_shoe:': '🥿', + ':womans_hat:': '👒', + ':womens:': '🚺', + ':woozy_face:': '🥴', + ':worried:': '😟', + ':wrench:': '🔧', + ':yarn:': '🧶', + ':yawning_face:': '🥱', + ':yellow_circle:': '🟡', + ':yellow_heart:': '💛', + ':yellow_square:': '🟨', + ':yen:': '💴', + ':yo_yo:': '🪀', + ':yum:': '😋', + ':zany_face:': '🤪', + ':zebra:': '🦓', + ':zipper_mouth_face:': 'ðŸ¤', + ':zipper_mouth:': 'ðŸ¤', + ':zombie:': '🧟', + ':zzz:': '💤', + ':airplane:': '✈ï¸', + ':alarm_clock:': 'â°', + ':alembic:': 'âš—ï¸', + ':anchor:': 'âš“', + ':aquarius:': 'â™’', + ':aries:': '♈', + ':arrow_backward:': 'â—€ï¸', + ':arrow_double_down:': 'â¬', + ':arrow_double_up:': 'â«', + ':arrow_down:': '⬇ï¸', + ':arrow_forward:': 'â–¶ï¸', + ':arrow_heading_down:': '⤵ï¸', + ':arrow_heading_up:': '⤴ï¸', + ':arrow_left:': '⬅ï¸', + ':arrow_lower_left:': '↙ï¸', + ':arrow_lower_right:': '↘ï¸', + ':arrow_right:': 'âž¡ï¸', + ':arrow_right_hook:': '↪ï¸', + ':arrow_up:': '⬆ï¸', + ':arrow_up_down:': '↕ï¸', + ':arrow_upper_left:': '↖ï¸', + ':arrow_upper_right:': '↗ï¸', + ':asterisk_symbol:': '*ï¸', + ':atom_symbol:': 'âš›ï¸', + ':atom:': 'âš›ï¸', + ':ballot_box_with_check:': '☑ï¸', + ':bangbang:': '‼ï¸', + ':baseball:': 'âš¾', + ':umbrella_on_ground:': 'â›±ï¸', + ':beach_umbrella:': 'â›±ï¸', + ':biohazard_sign:': '☣ï¸', + ':biohazard:': '☣ï¸', + ':black_circle:': 'âš«', + ':black_large_square:': '⬛', + ':black_medium_small_square:': 'â—¾', + ':black_medium_square:': 'â—¼ï¸', + ':black_nib:': '✒ï¸', + ':black_small_square:': 'â–ªï¸', + ':cancer:': '♋', + ':capricorn:': '♑', + ':chains:': '⛓ï¸', + ':chess_pawn:': '♟ï¸', + ':church:': '⛪', + ':cloud:': 'â˜ï¸', + ':clubs:': '♣ï¸', + ':coffee:': '☕', + ':coffin:': 'âš°ï¸', + ':comet:': '☄ï¸', + ':congratulations:': '㊗ï¸', + ':copyright:': '©ï¸', + ':latin_cross:': 'âœï¸', + ':cross:': 'âœï¸', + ':crossed_swords:': 'âš”ï¸', + ':curly_loop:': 'âž°', + ':diamonds:': '♦ï¸', + ':digit_eight:': '8ï¸', + ':digit_five:': '5ï¸', + ':digit_four:': '4ï¸', + ':digit_nine:': '9ï¸', + ':digit_one:': '1ï¸', + ':digit_seven:': '7ï¸', + ':digit_six:': '6ï¸', + ':digit_three:': '3ï¸', + ':digit_two:': '2ï¸', + ':digit_zero:': '0ï¸', + ':eight_pointed_black_star:': '✴ï¸', + ':eight_spoked_asterisk:': '✳ï¸', + ':eject_symbol:': 'âï¸', + ':eject:': 'âï¸', + ':envelope:': '✉ï¸', + ':exclamation:': 'â—', + ':fast_forward:': 'â©', + ':female_sign:': '♀ï¸', + ':ferry:': 'â›´ï¸', + ':fist:': '✊', + ':fleur-de-lis:': 'âšœï¸', + ':fountain:': '⛲', + ':white_frowning_face:': '☹ï¸', + ':frowning2:': '☹ï¸', + ':fuelpump:': '⛽', + ':gear:': 'âš™ï¸', + ':gemini:': '♊', + ':golf:': '⛳', + ':grey_exclamation:': 'â•', + ':grey_question:': 'â”', + ':hammer_and_pick:': 'âš’ï¸', + ':hammer_pick:': 'âš’ï¸', + ':heart:': 'â¤ï¸', + ':heavy_heart_exclamation_mark_ornament:': 'â£ï¸', + ':heart_exclamation:': 'â£ï¸', + ':hearts:': '♥ï¸', + ':heavy_check_mark:': '✔ï¸', + ':heavy_division_sign:': 'âž—', + ':heavy_minus_sign:': 'âž–', + ':heavy_multiplication_x:': '✖ï¸', + ':heavy_plus_sign:': 'âž•', + ':helmet_with_white_cross:': '⛑ï¸', + ':helmet_with_cross:': '⛑ï¸', + ':hotsprings:': '♨ï¸', + ':hourglass:': '⌛', + ':hourglass_flowing_sand:': 'â³', + ':ice_skate:': '⛸ï¸', + ':infinity:': '♾ï¸', + ':information_source:': 'ℹï¸', + ':interrobang:': 'â‰ï¸', + ':keyboard:': '⌨ï¸', + ':left_right_arrow:': '↔ï¸', + ':leftwards_arrow_with_hook:': '↩ï¸', + ':leo:': '♌', + ':libra:': '♎', + ':loop:': 'âž¿', + ':m:': 'â“‚ï¸', + ':male_sign:': '♂ï¸', + ':medical_symbol:': 'âš•ï¸', + ':mountain:': 'â›°ï¸', + ':negative_squared_cross_mark:': 'âŽ', + ':no_entry:': 'â›”', + ':o:': 'â•', + ':ophiuchus:': '⛎', + ':orthodox_cross:': '☦ï¸', + ':part_alternation_mark:': '〽ï¸', + ':partly_sunny:': 'â›…', + ':double_vertical_bar:': 'â¸ï¸', + ':pause_button:': 'â¸ï¸', + ':peace_symbol:': '☮ï¸', + ':peace:': '☮ï¸', + ':pencil2:': 'âœï¸', + ':basketball_player:': '⛹ï¸', + ':person_with_ball:': '⛹ï¸', + ':person_bouncing_ball:': '⛹ï¸', + ':pick:': 'â›ï¸', + ':pisces:': '♓', + ':play_pause:': 'â¯ï¸', + ':point_up:': 'â˜ï¸', + ':pound_symbol:': '#ï¸', + ':question:': 'â“', + ':radioactive_sign:': '☢ï¸', + ':radioactive:': '☢ï¸', + ':raised_hand:': '✋', + ':record_button:': 'âºï¸', + ':recycle:': 'â™»ï¸', + ':registered:': '®ï¸', + ':relaxed:': '☺ï¸', + ':rewind:': 'âª', + ':sagittarius:': 'â™', + ':sailboat:': '⛵', + ':scales:': 'âš–ï¸', + ':scissors:': '✂ï¸', + ':scorpius:': 'â™', + ':secret:': '㊙ï¸', + ':shamrock:': '☘ï¸', + ':shinto_shrine:': '⛩ï¸', + ':skier:': 'â›·ï¸', + ':skull_and_crossbones:': '☠ï¸', + ':skull_crossbones:': '☠ï¸', + ':snowflake:': 'â„ï¸', + ':snowman2:': '☃ï¸', + ':snowman:': '⛄', + ':soccer:': 'âš½', + ':spades:': 'â™ ï¸', + ':sparkle:': 'â‡ï¸', + ':sparkles:': '✨', + ':star:': 'â', + ':star_and_crescent:': '☪ï¸', + ':star_of_david:': '✡ï¸', + ':stop_button:': 'â¹ï¸', + ':stopwatch:': 'â±ï¸', + ':sunny:': '☀ï¸', + ':taurus:': '♉', + ':telephone:': '☎ï¸', + ':tent:': '⛺', + ':thunder_cloud_and_rain:': '⛈ï¸', + ':thunder_cloud_rain:': '⛈ï¸', + ':timer_clock:': 'â²ï¸', + ':timer:': 'â²ï¸', + ':tm:': 'â„¢ï¸', + ':next_track:': 'âï¸', + ':track_next:': 'âï¸', + ':previous_track:': 'â®ï¸', + ':track_previous:': 'â®ï¸', + ':umbrella2:': '☂ï¸', + ':umbrella:': '☔', + ':funeral_urn:': 'âš±ï¸', + ':urn:': 'âš±ï¸', + ':v:': '✌ï¸', + ':virgo:': 'â™', + ':warning:': 'âš ï¸', + ':watch:': '⌚', + ':wavy_dash:': '〰ï¸', + ':wheel_of_dharma:': '☸ï¸', + ':wheelchair:': '♿', + ':white_check_mark:': '✅', + ':white_circle:': '⚪', + ':white_large_square:': '⬜', + ':white_medium_small_square:': 'â—½', + ':white_medium_square:': 'â—»ï¸', + ':white_small_square:': 'â–«ï¸', + ':writing_hand:': 'âœï¸', + ':x:': 'âŒ', + ':yin_yang:': '☯ï¸', + ':zap:': 'âš¡', +}; export default emojis; diff --git a/packages/livechat/src/components/Emoji/shortnameToUnicode.js b/packages/livechat/src/components/Emoji/shortnameToUnicode.js index a90185a5c74..a34abb18a02 100644 --- a/packages/livechat/src/components/Emoji/shortnameToUnicode.js +++ b/packages/livechat/src/components/Emoji/shortnameToUnicode.js @@ -3,7 +3,7 @@ import emojis from './emojis'; const shortnamePattern = new RegExp(/:[-+_a-z0-9]+:/, 'gi'); const replaceShortNameWithUnicode = (shortname) => emojis[shortname] || shortname; -const regAscii = new RegExp(`((\\s|^)${ asciiRegexp }(?=\\s|$|[!,.?]))`, 'gi'); +const regAscii = new RegExp(`((\\s|^)${asciiRegexp}(?=\\s|$|[!,.?]))`, 'gi'); const unescapeHTML = (string) => { const unescaped = { @@ -19,12 +19,12 @@ const unescapeHTML = (string) => { '"': '"', '"': '"', '"': '"', - ''': '\'', - ''': '\'', - ''': '\'', + ''': "'", + ''': "'", + ''': "'", }; - return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/ig, (match) => unescaped[match]); + return string.replace(/&(?:amp|#38|#x26|lt|#60|#x3C|gt|#62|#x3E|apos|#39|#x27|quot|#34|#x22);/gi, (match) => unescaped[match]); }; const shortnameToUnicode = (stringMessage) => { diff --git a/packages/livechat/src/components/FilesDropTarget/index.js b/packages/livechat/src/components/FilesDropTarget/index.js index c6651087cde..2796cc79e8d 100644 --- a/packages/livechat/src/components/FilesDropTarget/index.js +++ b/packages/livechat/src/components/FilesDropTarget/index.js @@ -3,7 +3,6 @@ import { Component } from 'preact'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - const escapeForRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); export class FilesDropTarget extends Component { @@ -57,19 +56,18 @@ export class FilesDropTarget extends Component { let filteredFiles = Array.from(files); if (accept) { - const acceptMatchers = accept.split(',') - .map((acceptString) => { - if (acceptString.charAt(0) === '.') { - return ({ name }) => new RegExp(`${ escapeForRegExp(acceptString) }$`, 'i').test(name); - } + const acceptMatchers = accept.split(',').map((acceptString) => { + if (acceptString.charAt(0) === '.') { + return ({ name }) => new RegExp(`${escapeForRegExp(acceptString)}$`, 'i').test(name); + } - const matchTypeOnly = /^(.+)\/\*$/i.exec(acceptString); - if (matchTypeOnly) { - return ({ type }) => new RegExp(`^${ escapeForRegExp(matchTypeOnly[1]) }/.*$`, 'i').test(type); - } + const matchTypeOnly = /^(.+)\/\*$/i.exec(acceptString); + if (matchTypeOnly) { + return ({ type }) => new RegExp(`^${escapeForRegExp(matchTypeOnly[1])}/.*$`, 'i').test(type); + } - return ({ type }) => new RegExp(`^s${ escapeForRegExp(acceptString) }$`, 'i').test(type); - }); + return ({ type }) => new RegExp(`^s${escapeForRegExp(acceptString)}$`, 'i').test(type); + }); filteredFiles = filteredFiles.filter((file) => acceptMatchers.some((acceptMatcher) => acceptMatcher(file))); } @@ -85,15 +83,7 @@ export class FilesDropTarget extends Component { this.input.click(); }; - render = ({ - overlayed, - overlayText, - accept, - multiple, - className, - style = {}, - children, - }, { dragLevel }) => ( + render = ({ overlayed, overlayText, accept, multiple, className, style = {}, children }, { dragLevel }) => ( <div data-overlay-text={overlayText} onDragOver={this.handleDragOver} diff --git a/packages/livechat/src/components/FilesDropTarget/stories.js b/packages/livechat/src/components/FilesDropTarget/stories.js index c12ee30a499..acd2a32a331 100644 --- a/packages/livechat/src/components/FilesDropTarget/stories.js +++ b/packages/livechat/src/components/FilesDropTarget/stories.js @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { FilesDropTarget } from '.'; import { centered } from '../../helpers.stories'; - const DummyContent = () => ( <div style={{ diff --git a/packages/livechat/src/components/Footer/index.js b/packages/livechat/src/components/Footer/index.js index 1d011199456..dd7270cd7ca 100644 --- a/packages/livechat/src/components/Footer/index.js +++ b/packages/livechat/src/components/Footer/index.js @@ -11,14 +11,12 @@ export const Footer = ({ children, className, ...props }) => ( </footer> ); - export const FooterContent = ({ children, className, ...props }) => ( <div className={createClassName(styles, 'footer__content', {}, [className])} {...props}> {children} </div> ); - export const PoweredBy = withTranslation()(({ className, t, ...props }) => ( <h3 className={createClassName(styles, 'powered-by', {}, [className])} {...props}> {t('powered_by_rocket_chat').split('Rocket.Chat')[0]} @@ -29,7 +27,6 @@ export const PoweredBy = withTranslation()(({ className, t, ...props }) => ( </h3> )); - const handleMouseUp = ({ target }) => target.blur(); const OptionsTrigger = withTranslation()(({ pop, t }) => ( @@ -38,19 +35,14 @@ const OptionsTrigger = withTranslation()(({ pop, t }) => ( </button> )); - export const FooterOptions = ({ children }) => ( <PopoverMenu trigger={OptionsTrigger} overlayed> {children} </PopoverMenu> ); - export const CharCounter = ({ className, style = {}, textLength, limitTextLength }) => ( - <span - className={createClassName(styles, 'footer__remainder', { highlight: textLength === limitTextLength }, [className])} - style={style} - > + <span className={createClassName(styles, 'footer__remainder', { highlight: textLength === limitTextLength }, [className])} style={style}> {textLength} / {limitTextLength} </span> ); diff --git a/packages/livechat/src/components/Footer/stories.js b/packages/livechat/src/components/Footer/stories.js index 9235a553a3a..d9c813e4a26 100644 --- a/packages/livechat/src/components/Footer/stories.js +++ b/packages/livechat/src/components/Footer/stories.js @@ -10,7 +10,6 @@ import { Composer } from '../Composer'; import Menu from '../Menu'; import { PopoverContainer } from '../Popover'; - const bottomWithPopoverContainer = (storyFn) => ( <div style={{ display: 'flex', width: '100vw', height: '100vh' }}> <PopoverContainer> @@ -37,9 +36,15 @@ storiesOf('Components/Footer', module) <FooterContent> <FooterOptions> <Menu.Group> - <Menu.Item onClick={action('change-department')} icon={ChangeIcon}>Change department</Menu.Item> - <Menu.Item onClick={action('remove-user-data')} icon={RemoveIcon}>Forget/Remove my personal data</Menu.Item> - <Menu.Item danger onClick={action('finish-chat')} icon={FinishIcon}>Finish this chat</Menu.Item> + <Menu.Item onClick={action('change-department')} icon={ChangeIcon}> + Change department + </Menu.Item> + <Menu.Item onClick={action('remove-user-data')} icon={RemoveIcon}> + Forget/Remove my personal data + </Menu.Item> + <Menu.Item danger onClick={action('finish-chat')} icon={FinishIcon}> + Finish this chat + </Menu.Item> </Menu.Group> </FooterOptions> <PoweredBy /> diff --git a/packages/livechat/src/components/Form/DateInput/index.js b/packages/livechat/src/components/Form/DateInput/index.js index c05cf5c5a5a..1eae79fae6d 100644 --- a/packages/livechat/src/components/Form/DateInput/index.js +++ b/packages/livechat/src/components/Form/DateInput/index.js @@ -1,18 +1,7 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; -const DateInput = ({ - name, - value, - placeholder, - disabled, - small, - error, - onChange, - onInput, - className, - style = {}, -}) => ( +const DateInput = ({ name, value, placeholder, disabled, small, error, onChange, onInput, className, style = {} }) => ( <input type='date' name={name} diff --git a/packages/livechat/src/components/Form/FormField/index.js b/packages/livechat/src/components/Form/FormField/index.js index 1f3e13535d5..52f6f0f8381 100644 --- a/packages/livechat/src/components/Form/FormField/index.js +++ b/packages/livechat/src/components/Form/FormField/index.js @@ -3,33 +3,14 @@ import { cloneElement } from 'preact'; import { createClassName } from '../../helpers'; import styles from './styles.scss'; - -export const FormField = ({ - required, - label, - description, - error, - className, - style = {}, - children, -}) => ( - <div - className={createClassName(styles, 'form-field', { required, error: !!error }, [className])} - style={style} - > +export const FormField = ({ required, label, description, error, className, style = {}, children }) => ( + <div className={createClassName(styles, 'form-field', { required, error: !!error }, [className])} style={style}> <label className={createClassName(styles, 'form-field__label-wrapper')}> - {label - ? <span className={createClassName(styles, 'form-field__label')}>{label}</span> - : null} + {label ? <span className={createClassName(styles, 'form-field__label')}>{label}</span> : null} <span className={createClassName(styles, 'form-field__input')}> - {error - ? (Array.isArray(children) ? children : [children]) - .map((child) => cloneElement(child, { error: !!error })) - : children} + {error ? (Array.isArray(children) ? children : [children]).map((child) => cloneElement(child, { error: !!error })) : children} </span> </label> - <small className={createClassName(styles, 'form-field__description')}> - {error || description} - </small> + <small className={createClassName(styles, 'form-field__description')}>{error || description}</small> </div> ); diff --git a/packages/livechat/src/components/Form/FormField/stories.js b/packages/livechat/src/components/Form/FormField/stories.js index 8f12916115f..aacf6bc9b7a 100644 --- a/packages/livechat/src/components/Form/FormField/stories.js +++ b/packages/livechat/src/components/Form/FormField/stories.js @@ -5,7 +5,6 @@ import { FormField } from '.'; import { Form, TextInput } from '..'; import { loremIpsum, centered } from '../../../helpers.stories'; - storiesOf('Forms/FormField', module) .addDecorator(centered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/components/Form/PasswordInput/index.js b/packages/livechat/src/components/Form/PasswordInput/index.js index 688b00e4bd0..dbdc592fe91 100644 --- a/packages/livechat/src/components/Form/PasswordInput/index.js +++ b/packages/livechat/src/components/Form/PasswordInput/index.js @@ -1,19 +1,7 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - -export const PasswordInput = memo(({ - name, - value, - placeholder, - disabled, - small, - error, - onChange, - onInput, - className, - style = {}, -}) => ( +export const PasswordInput = memo(({ name, value, placeholder, disabled, small, error, onChange, onInput, className, style = {} }) => ( <input type='password' name={name} diff --git a/packages/livechat/src/components/Form/PasswordInput/stories.js b/packages/livechat/src/components/Form/PasswordInput/stories.js index 8ef93211e51..31fd719690a 100644 --- a/packages/livechat/src/components/Form/PasswordInput/stories.js +++ b/packages/livechat/src/components/Form/PasswordInput/stories.js @@ -6,7 +6,6 @@ import { PasswordInput } from '.'; import { Form, FormField } from '..'; import { centered } from '../../../helpers.stories'; - storiesOf('Forms/PasswordInput', module) .addDecorator(centered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/components/Form/SelectInput/index.js b/packages/livechat/src/components/Form/SelectInput/index.js index 6aad4123dde..a92e6a19fe4 100644 --- a/packages/livechat/src/components/Form/SelectInput/index.js +++ b/packages/livechat/src/components/Form/SelectInput/index.js @@ -4,7 +4,6 @@ import ArrowIcon from '../../../icons/arrowDown.svg'; import { createClassName } from '../../helpers'; import styles from './styles.scss'; - export class SelectInput extends Component { static getDerivedStateFromProps(props, state) { if (props.value !== state.value) { @@ -29,22 +28,8 @@ export class SelectInput extends Component { this.setState({ value: event.target.value }); }; - render = ({ - name, - placeholder, - options = [], - disabled, - small, - error, - onInput, - className, - style = {}, - ...props - }) => ( - <div - className={createClassName(styles, 'select-input', {}, [className])} - style={style} - > + render = ({ name, placeholder, options = [], disabled, small, error, onInput, className, style = {}, ...props }) => ( + <div className={createClassName(styles, 'select-input', {}, [className])} style={style}> <select name={name} value={this.state.value} @@ -59,9 +44,13 @@ export class SelectInput extends Component { })} {...props} > - <option value='' disabled hidden>{placeholder}</option> + <option value='' disabled hidden> + {placeholder} + </option> {Array.from(options).map(({ value, label }, key) => ( - <option key={key} value={value} className={createClassName(styles, 'select-input__option')}>{label}</option> + <option key={key} value={value} className={createClassName(styles, 'select-input__option')}> + {label} + </option> ))} </select> <ArrowIcon className={createClassName(styles, 'select-input__arrow')} /> diff --git a/packages/livechat/src/components/Form/SelectInput/stories.js b/packages/livechat/src/components/Form/SelectInput/stories.js index 6ec401a1037..3df2e42133f 100644 --- a/packages/livechat/src/components/Form/SelectInput/stories.js +++ b/packages/livechat/src/components/Form/SelectInput/stories.js @@ -6,7 +6,6 @@ import { SelectInput } from '.'; import { Form, FormField } from '..'; import { centered } from '../../../helpers.stories'; - storiesOf('Forms/SelectInput', module) .addDecorator(centered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/components/Form/TextInput/index.js b/packages/livechat/src/components/Form/TextInput/index.js index d92ab333473..ac8091afd63 100644 --- a/packages/livechat/src/components/Form/TextInput/index.js +++ b/packages/livechat/src/components/Form/TextInput/index.js @@ -1,24 +1,9 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - -export const TextInput = memo(({ - name, - value, - placeholder, - disabled, - small, - multiline = false, - rows = 1, - error, - onChange, - onInput, - className, - style = {}, - ...props -}) => ( - multiline - ? ( +export const TextInput = memo( + ({ name, value, placeholder, disabled, small, multiline = false, rows = 1, error, onChange, onInput, className, style = {}, ...props }) => + multiline ? ( <textarea rows={rows} name={name} @@ -31,8 +16,7 @@ export const TextInput = memo(({ style={style} {...props} /> - ) - : ( + ) : ( <input type='text' name={name} @@ -45,5 +29,5 @@ export const TextInput = memo(({ style={style} {...props} /> - ) -)); + ), +); diff --git a/packages/livechat/src/components/Form/TextInput/stories.js b/packages/livechat/src/components/Form/TextInput/stories.js index b24f540086c..0b05bf0e1d5 100644 --- a/packages/livechat/src/components/Form/TextInput/stories.js +++ b/packages/livechat/src/components/Form/TextInput/stories.js @@ -6,7 +6,6 @@ import { TextInput } from '.'; import { Form, FormField } from '..'; import { centered } from '../../../helpers.stories'; - storiesOf('Forms/TextInput', module) .addDecorator(centered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/components/Form/index.js b/packages/livechat/src/components/Form/index.js index 2add06732a1..b644a4d1384 100644 --- a/packages/livechat/src/components/Form/index.js +++ b/packages/livechat/src/components/Form/index.js @@ -27,7 +27,6 @@ export const Validations = { custom: ({ value, pattern }) => (new RegExp(pattern, 'i').test(String(value)) ? null : i18next.t('invalid_value')), }; - export { FormField } from './FormField'; export { TextInput } from './TextInput'; export { PasswordInput } from './PasswordInput'; diff --git a/packages/livechat/src/components/Form/stories.js b/packages/livechat/src/components/Form/stories.js index d78070ae63f..c9b95dc4bf6 100644 --- a/packages/livechat/src/components/Form/stories.js +++ b/packages/livechat/src/components/Form/stories.js @@ -1,18 +1,12 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import { - Form, - PasswordInput, - SelectInput, - TextInput, -} from '.'; +import { Form, PasswordInput, SelectInput, TextInput } from '.'; import { centered } from '../../helpers.stories'; import { Button } from '../Button'; import { ButtonGroup } from '../ButtonGroup'; import { FormField } from './FormField'; - function handleSubmit(event, ...args) { event.preventDefault(); action('submit')(event, ...args); @@ -22,22 +16,13 @@ storiesOf('Forms/Form', module) .addDecorator(centered) .add('default', () => ( <Form onSubmit={handleSubmit}> - <FormField - label='Text' - description='Input field for plain text' - > + <FormField label='Text' description='Input field for plain text'> <TextInput /> </FormField> - <FormField - label='Password' - description='Input field for secret text' - > + <FormField label='Password' description='Input field for secret text'> <PasswordInput /> </FormField> - <FormField - label='Select' - description='Input field for secret text' - > + <FormField label='Select' description='Input field for secret text'> <SelectInput options={[ { value: '1', label: 'Option 1' }, @@ -47,8 +32,12 @@ storiesOf('Forms/Form', module) /> </FormField> <ButtonGroup> - <Button submit stack>Submit</Button> - <Button nude secondary stack>Cancel</Button> + <Button submit stack> + Submit + </Button> + <Button nude secondary stack> + Cancel + </Button> </ButtonGroup> </Form> )); diff --git a/packages/livechat/src/components/Header/index.js b/packages/livechat/src/components/Header/index.js index 27e3783fd6f..85c7371d2ee 100644 --- a/packages/livechat/src/components/Header/index.js +++ b/packages/livechat/src/components/Header/index.js @@ -3,19 +3,10 @@ import { toChildArray } from 'preact'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - -export const Header = ({ - children, - theme: { color: backgroundColor, fontColor: color } = {}, - className, - post, - large, - style, - ...props -}) => ( +export const Header = ({ children, theme: { color: backgroundColor, fontColor: color } = {}, className, post, large, style, ...props }) => ( <header className={createClassName(styles, 'header', { large }, [className])} - style={style || backgroundColor || color ? { ...style || {}, backgroundColor, color } : null} + style={style || backgroundColor || color ? { ...(style || {}), backgroundColor, color } : null} {...props} > {children} @@ -43,9 +34,14 @@ export const Title = ({ children, className, ...props }) => ( export const SubTitle = ({ children, className, ...props }) => ( <div - className={createClassName(styles, 'header__subtitle', { - children: toChildArray(children).length > 0, - }, [className])} + className={createClassName( + styles, + 'header__subtitle', + { + children: toChildArray(children).length > 0, + }, + [className], + )} {...props} > {children} @@ -71,10 +67,7 @@ export const Post = ({ children, className, ...props }) => ( ); export const CustomField = ({ children, className, ...props }) => ( - <div - className={createClassName(styles, 'header__custom-field', {}, [className])} - {...props} - > + <div className={createClassName(styles, 'header__custom-field', {}, [className])} {...props}> {children} </div> ); diff --git a/packages/livechat/src/components/Header/stories.js b/packages/livechat/src/components/Header/stories.js index 2fe63395ed8..6bf57d4b9b9 100644 --- a/packages/livechat/src/components/Header/stories.js +++ b/packages/livechat/src/components/Header/stories.js @@ -10,7 +10,6 @@ import NewWindow from '../../icons/newWindow.svg'; import Alert from '../Alert'; import { Avatar } from '../Avatar'; - const avatarSrc = avatarResolver('guilherme.gazzo'); storiesOf('Components/Header', module) @@ -23,9 +22,7 @@ storiesOf('Components/Header', module) }} onClick={action('clicked')} > - <Content> - {text('text', 'Need Help?')} - </Content> + <Content>{text('text', 'Need Help?')}</Content> </Header> )) .add('with long text content', () => ( @@ -36,9 +33,7 @@ storiesOf('Components/Header', module) }} onClick={action('clicked')} > - <Content> - {text('text', 'Need Help? '.repeat(100))} - </Content> + <Content>{text('text', 'Need Help? '.repeat(100))}</Content> </Header> )) .add('with title and subtitle', () => ( @@ -67,9 +62,7 @@ storiesOf('Components/Header', module) <Bell width={20} height={20} /> </Picture> - <Content> - {text('text', 'Notification settings')} - </Content> + <Content>{text('text', 'Notification settings')}</Content> </Header> )) .add('with actions', () => ( @@ -80,9 +73,7 @@ storiesOf('Components/Header', module) }} onClick={action('clicked')} > - <Content> - {text('text', 'Chat finished')} - </Content> + <Content>{text('text', 'Chat finished')}</Content> <Actions> <Action onClick={action('notifications')}> @@ -102,37 +93,22 @@ storiesOf('Components/Header', module) }} post={ <Post> - <Alert - success={boolean('success', true)} - onDismiss={action('clicked')} - > + <Alert success={boolean('success', true)} onDismiss={action('clicked')}> {text('text', 'Success')} </Alert> - <Alert - warning={boolean('warning', true)} - onDismiss={action('clicked')} - > + <Alert warning={boolean('warning', true)} onDismiss={action('clicked')}> {text('text', 'Warning')} </Alert> - <Alert - error={boolean('error', true)} - onDismiss={action('clicked')} - > + <Alert error={boolean('error', true)} onDismiss={action('clicked')}> {text('text', 'Error')} </Alert> - <Alert - error={boolean('error', true)} - color={color('color', '#175CC4')} - onDismiss={action('clicked')} - > + <Alert error={boolean('error', true)} color={color('color', '#175CC4')} onDismiss={action('clicked')}> {text('text', 'Custom color')} </Alert> </Post> } > - <Content> - {text('text', 'Chat finished')} - </Content> + <Content>{text('text', 'Chat finished')}</Content> <Actions> <Action onClick={action('notifications')}> @@ -157,9 +133,7 @@ storiesOf('Components/Header', module) <Content> <Title>{text('title', '@guilherme.gazzo')}</Title> - <SubTitle> - {text('subtitle', 'guilherme.gazzo@rocket.chat')} - </SubTitle> + <SubTitle>{text('subtitle', 'guilherme.gazzo@rocket.chat')}</SubTitle> </Content> <Actions> @@ -189,12 +163,8 @@ storiesOf('Components/Header', module) <Content> <Title>{text('title', 'Guilherme Gazzo')}</Title> - <SubTitle> - {text('subtitle', 'guilherme.gazzo@rocket.chat')} - </SubTitle> - <CustomField> - {text('custom', '+ 55 42423 24242')} - </CustomField> + <SubTitle>{text('subtitle', 'guilherme.gazzo@rocket.chat')}</SubTitle> + <CustomField>{text('custom', '+ 55 42423 24242')}</CustomField> </Content> <Actions> @@ -218,16 +188,10 @@ storiesOf('Components/Header', module) }} post={ <Post> - <Alert - success={boolean('success', true)} - onDismiss={action('clicked')} - > + <Alert success={boolean('success', true)} onDismiss={action('clicked')}> {text('text', 'Success')} </Alert> - <Alert - warning={boolean('warning', true)} - onDismiss={action('clicked')} - > + <Alert warning={boolean('warning', true)} onDismiss={action('clicked')}> {text('text', 'Warning')} </Alert> </Post> @@ -240,12 +204,8 @@ storiesOf('Components/Header', module) <Content> <Title>{text('title', 'Guilherme Gazzo')}</Title> - <SubTitle> - {text('subtitle', 'guilherme.gazzo@rocket.chat')} - </SubTitle> - <CustomField> - {text('custom', '+ 55 42423 24242')} - </CustomField> + <SubTitle>{text('subtitle', 'guilherme.gazzo@rocket.chat')}</SubTitle> + <CustomField>{text('custom', '+ 55 42423 24242')}</CustomField> </Content> <Actions> @@ -275,12 +235,8 @@ storiesOf('Components/Header', module) <Content> <Title>{text('title', 'Guilherme Gazzo')}</Title> - <SubTitle> - {text('subtitle', 'guilherme.gazzo@rocket.chat')} - </SubTitle> - <CustomField> - {text('custom', '+ 55 42423 24242')} - </CustomField> + <SubTitle>{text('subtitle', 'guilherme.gazzo@rocket.chat')}</SubTitle> + <CustomField>{text('custom', '+ 55 42423 24242')}</CustomField> </Content> <Actions> diff --git a/packages/livechat/src/components/Menu/index.js b/packages/livechat/src/components/Menu/index.js index d544e48ed80..336c3935ba7 100644 --- a/packages/livechat/src/components/Menu/index.js +++ b/packages/livechat/src/components/Menu/index.js @@ -4,14 +4,12 @@ import { PopoverTrigger } from '../Popover'; import { createClassName, normalizeDOMRect } from '../helpers'; import styles from './styles.scss'; - export const Menu = ({ children, hidden, placement, ...props }) => ( <div className={createClassName(styles, 'menu', { hidden, placement })} {...props}> {children} </div> ); - export const Group = ({ children, title, ...props }) => ( <div className={createClassName(styles, 'menu__group')} {...props}> {title && <div className={createClassName(styles, 'menu__group-title')}>{title}</div>} @@ -19,20 +17,12 @@ export const Group = ({ children, title, ...props }) => ( </div> ); - -export const Item = ({ children, primary, danger, disabled, icon, ...props }) => - <button - className={createClassName(styles, 'menu__item', { primary, danger, disabled })} - disabled={disabled} - {...props} - > - {icon && ( - <div className={createClassName(styles, 'menu__item__icon')}> - {icon()} - </div> - )} +export const Item = ({ children, primary, danger, disabled, icon, ...props }) => ( + <button className={createClassName(styles, 'menu__item', { primary, danger, disabled })} disabled={disabled} {...props}> + {icon && <div className={createClassName(styles, 'menu__item__icon')}>{icon()}</div>} {children} - </button>; + </button> +); class PopoverMenuWrapper extends Component { state = {}; @@ -41,7 +31,7 @@ class PopoverMenuWrapper extends Component { }; handleClick = ({ target }) => { - if (!target.closest(`.${ styles.menu__item }`)) { + if (!target.closest(`.${styles.menu__item}`)) { return; } @@ -65,7 +55,7 @@ class PopoverMenuWrapper extends Component { const top = menuHeight < bottomSpace ? triggerBounds.bottom : null; const bottom = menuHeight < bottomSpace ? null : overlayBounds.bottom - triggerBounds.top; - const placement = `${ menuWidth < rightSpace ? 'right' : 'left' }-${ menuHeight < bottomSpace ? 'bottom' : 'top' }`; + const placement = `${menuWidth < rightSpace ? 'right' : 'left'}-${menuHeight < bottomSpace ? 'bottom' : 'top'}`; // eslint-disable-next-line react/no-did-mount-set-state this.setState({ @@ -86,7 +76,6 @@ class PopoverMenuWrapper extends Component { ); } - export const PopoverMenu = ({ children, trigger, overlayed }) => ( <PopoverTrigger overlayProps={{ @@ -95,21 +84,15 @@ export const PopoverMenu = ({ children, trigger, overlayed }) => ( > {trigger} {({ dismiss, triggerBounds, overlayBounds }) => ( - <PopoverMenuWrapper - dismiss={dismiss} - triggerBounds={triggerBounds} - overlayBounds={overlayBounds} - > + <PopoverMenuWrapper dismiss={dismiss} triggerBounds={triggerBounds} overlayBounds={overlayBounds}> {children} </PopoverMenuWrapper> )} </PopoverTrigger> ); - Menu.Group = Group; Menu.Item = Item; Menu.Popover = PopoverMenu; - export default Menu; diff --git a/packages/livechat/src/components/Menu/stories.js b/packages/livechat/src/components/Menu/stories.js index 4faa780fc20..00605a4f75c 100644 --- a/packages/livechat/src/components/Menu/stories.js +++ b/packages/livechat/src/components/Menu/stories.js @@ -11,16 +11,13 @@ import FinishIcon from '../../icons/finish.svg'; import { Button } from '../Button'; import { PopoverContainer } from '../Popover'; - const defaultMenuItemText = 'A menu item'; const defaultAnotherMenuItemText = 'Another menu item'; storiesOf('Components/Menu', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('empty', () => ( - <Menu hidden={boolean('hidden', false)} /> - )) + .add('empty', () => <Menu hidden={boolean('hidden', false)} />) .add('simple', () => ( <Menu hidden={boolean('hidden menu', false)}> <Group title={text('group title', '')}> @@ -46,9 +43,7 @@ storiesOf('Components/Menu', module) const centeredWithPopoverContainer = (storyFn, ...args) => ( <div style={{ display: 'flex', width: '100vw', height: '100vh' }}> - <PopoverContainer> - {centered(storyFn, ...args)} - </PopoverContainer> + <PopoverContainer>{centered(storyFn, ...args)}</PopoverContainer> </div> ); @@ -122,7 +117,9 @@ storiesOf('Components/Menu/Item', module) .add('primary', () => ( <Menu hidden={boolean('hidden menu', false)}> <Group title={text('group title', '')}> - <Item primary onClick={action('clicked')}>{text('item text', defaultMenuItemText)}</Item> + <Item primary onClick={action('clicked')}> + {text('item text', defaultMenuItemText)} + </Item> <Item>{defaultAnotherMenuItemText}</Item> </Group> </Menu> @@ -130,7 +127,9 @@ storiesOf('Components/Menu/Item', module) .add('danger', () => ( <Menu hidden={boolean('hidden menu', false)}> <Group title={text('group title', '')}> - <Item danger onClick={action('clicked')}>{text('item text', defaultMenuItemText)}</Item> + <Item danger onClick={action('clicked')}> + {text('item text', defaultMenuItemText)} + </Item> <Item>{defaultAnotherMenuItemText}</Item> </Group> </Menu> @@ -138,7 +137,9 @@ storiesOf('Components/Menu/Item', module) .add('disabled', () => ( <Menu hidden={boolean('hidden menu', false)}> <Group title={text('group title', '')}> - <Item disabled onClick={action('clicked')}>{text('item text', defaultMenuItemText)}</Item> + <Item disabled onClick={action('clicked')}> + {text('item text', defaultMenuItemText)} + </Item> <Item>{defaultAnotherMenuItemText}</Item> </Group> </Menu> @@ -146,10 +147,18 @@ storiesOf('Components/Menu/Item', module) .add('with icon', () => ( <Menu hidden={boolean('hidden menu', false)}> <Group title={text('group title', '')}> - <Item onClick={action('clicked')} icon={CloseIcon}>{text('item text', 'Simple')}</Item> - <Item primary onClick={action('clicked')} icon={ChangeIcon}>{text('item text', 'Primary')}</Item> - <Item danger onClick={action('clicked')} icon={FinishIcon}>{text('item text', 'Danger')}</Item> - <Item disabled onClick={action('clicked')} icon={BellIcon}>{text('item text', 'Disabled')}</Item> + <Item onClick={action('clicked')} icon={CloseIcon}> + {text('item text', 'Simple')} + </Item> + <Item primary onClick={action('clicked')} icon={ChangeIcon}> + {text('item text', 'Primary')} + </Item> + <Item danger onClick={action('clicked')} icon={FinishIcon}> + {text('item text', 'Danger')} + </Item> + <Item disabled onClick={action('clicked')} icon={BellIcon}> + {text('item text', 'Disabled')} + </Item> </Group> </Menu> )); diff --git a/packages/livechat/src/components/Messages/AudioAttachment/index.js b/packages/livechat/src/components/Messages/AudioAttachment/index.js index 9bfe8d11af2..665fb462d2e 100644 --- a/packages/livechat/src/components/Messages/AudioAttachment/index.js +++ b/packages/livechat/src/components/Messages/AudioAttachment/index.js @@ -4,23 +4,9 @@ import { createClassName, memo } from '../../helpers'; import { MessageBubble } from '../MessageBubble'; import styles from './styles.scss'; - -const AudioAttachment = memo(({ - url, - className, - t, - ...messageBubbleProps -}) => ( - <MessageBubble - nude - className={createClassName(styles, 'audio-attachment', {}, [className])} - {...messageBubbleProps} - > - <audio - src={url} - controls - className={createClassName(styles, 'audio-attachment__inner')} - > +const AudioAttachment = memo(({ url, className, t, ...messageBubbleProps }) => ( + <MessageBubble nude className={createClassName(styles, 'audio-attachment', {}, [className])} {...messageBubbleProps}> + <audio src={url} controls className={createClassName(styles, 'audio-attachment__inner')}> {t('you_browser_doesn_t_support_audio_element')} </audio> </MessageBubble> diff --git a/packages/livechat/src/components/Messages/AudioAttachment/stories.js b/packages/livechat/src/components/Messages/AudioAttachment/stories.js index 2365c889b9e..1c1923bfcc1 100644 --- a/packages/livechat/src/components/Messages/AudioAttachment/stories.js +++ b/packages/livechat/src/components/Messages/AudioAttachment/stories.js @@ -5,12 +5,7 @@ import AudioAttachment from '.'; import sampleAudio from '../../../../.storybook/assets/sample-audio.mp3'; import { centered } from '../../../helpers.stories'; - storiesOf('Messages/AudioAttachment', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('default', () => ( - <AudioAttachment - url={text('url', sampleAudio)} - /> - )); + .add('default', () => <AudioAttachment url={text('url', sampleAudio)} />); diff --git a/packages/livechat/src/components/Messages/FileAttachment/index.js b/packages/livechat/src/components/Messages/FileAttachment/index.js index 79bd045174e..8f57b243ac4 100644 --- a/packages/livechat/src/components/Messages/FileAttachment/index.js +++ b/packages/livechat/src/components/Messages/FileAttachment/index.js @@ -4,24 +4,9 @@ import { FileAttachmentIcon } from '../FileAttachmentIcon'; import { MessageBubble } from '../MessageBubble'; import styles from './styles.scss'; - -export const FileAttachment = memo(({ - url, - title, - className, - ...messageBubbleProps -}) => ( - <MessageBubble - className={createClassName(styles, 'file-attachment', {}, [className])} - {...messageBubbleProps} - > - <a - href={url} - download - target='_blank' - rel='noopener noreferrer' - className={createClassName(styles, 'file-attachment__inner')} - > +export const FileAttachment = memo(({ url, title, className, ...messageBubbleProps }) => ( + <MessageBubble className={createClassName(styles, 'file-attachment', {}, [className])} {...messageBubbleProps}> + <a href={url} download target='_blank' rel='noopener noreferrer' className={createClassName(styles, 'file-attachment__inner')}> <FileAttachmentIcon url={url} /> <span className={createClassName(styles, 'file-attachment__title')}>{title}</span> <DownloadIcon width={20} height={20} className={createClassName(styles, 'file-attachment__download-button')} /> diff --git a/packages/livechat/src/components/Messages/FileAttachment/stories.js b/packages/livechat/src/components/Messages/FileAttachment/stories.js index 2b593eeeeb7..c7e130b96df 100644 --- a/packages/livechat/src/components/Messages/FileAttachment/stories.js +++ b/packages/livechat/src/components/Messages/FileAttachment/stories.js @@ -4,55 +4,17 @@ import { storiesOf } from '@storybook/react'; import { FileAttachment } from '.'; import { loremIpsum, centered } from '../../../helpers.stories'; - -const centeredWithWidth = (storyFn, ...args) => centered(() => ( - <div style={{ width: '365px' }}> - {storyFn()} - </div> -), ...args); +const centeredWithWidth = (storyFn, ...args) => centered(() => <div style={{ width: '365px' }}>{storyFn()}</div>, ...args); storiesOf('Messages/FileAttachment', module) .addDecorator(centeredWithWidth) .addDecorator(withKnobs) - .add('for pdf', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.pdf')} - /> - )) - .add('for doc', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.doc')} - /> - )) - .add('for ppt', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.ppt')} - /> - )) - .add('for xls', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.xls')} - /> - )) - .add('for zip', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.zip')} - /> - )) - .add('for unknown extension', () => ( - <FileAttachment - title={text('title', 'Untitle')} - url={text('url', 'http://example.com/demo.abc')} - /> - )) + .add('for pdf', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.pdf')} />) + .add('for doc', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.doc')} />) + .add('for ppt', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.ppt')} />) + .add('for xls', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.xls')} />) + .add('for zip', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.zip')} />) + .add('for unknown extension', () => <FileAttachment title={text('title', 'Untitle')} url={text('url', 'http://example.com/demo.abc')} />) .add('with long title', () => ( - <FileAttachment - title={text('title', loremIpsum({ count: 50, units: 'words' }))} - url={text('url', 'http://example.com/demo.abc')} - /> + <FileAttachment title={text('title', loremIpsum({ count: 50, units: 'words' }))} url={text('url', 'http://example.com/demo.abc')} /> )); diff --git a/packages/livechat/src/components/Messages/FileAttachmentIcon/index.js b/packages/livechat/src/components/Messages/FileAttachmentIcon/index.js index 30f28826a7e..dc56749a684 100644 --- a/packages/livechat/src/components/Messages/FileAttachmentIcon/index.js +++ b/packages/livechat/src/components/Messages/FileAttachmentIcon/index.js @@ -6,15 +6,15 @@ import SheetIcon from '../../../icons/sheet.svg'; import ZipIcon from '../../../icons/zip.svg'; import { memo } from '../../helpers'; - export const FileAttachmentIcon = memo(({ url }) => { const extension = url ? url.split('.').pop() : null; - const Icon = (/pdf/i.test(extension) && PDFIcon) - || (/doc|docx|rtf|txt|odt|pages|log/i.test(extension) && DocIcon) - || (/ppt|pptx|pps/i.test(extension) && PPTIcon) - || (/xls|xlsx|csv/i.test(extension) && SheetIcon) - || (/zip|rar|7z|gz/i.test(extension) && ZipIcon) - || FileIcon; + const Icon = + (/pdf/i.test(extension) && PDFIcon) || + (/doc|docx|rtf|txt|odt|pages|log/i.test(extension) && DocIcon) || + (/ppt|pptx|pps/i.test(extension) && PPTIcon) || + (/xls|xlsx|csv/i.test(extension) && SheetIcon) || + (/zip|rar|7z|gz/i.test(extension) && ZipIcon) || + FileIcon; return <Icon width={32} />; }); diff --git a/packages/livechat/src/components/Messages/ImageAttachment/index.js b/packages/livechat/src/components/Messages/ImageAttachment/index.js index 60194c85149..0f7455dca5f 100644 --- a/packages/livechat/src/components/Messages/ImageAttachment/index.js +++ b/packages/livechat/src/components/Messages/ImageAttachment/index.js @@ -2,20 +2,8 @@ import { createClassName, memo } from '../../helpers'; import { MessageBubble } from '../MessageBubble'; import styles from './styles.scss'; - -export const ImageAttachment = memo(({ - url, - className, - ...messageBubbleProps -}) => ( - <MessageBubble - nude - className={createClassName(styles, 'image-attachment', {}, [className])} - {...messageBubbleProps} - > - <img - src={url} - className={createClassName(styles, 'image-attachment__inner')} - /> +export const ImageAttachment = memo(({ url, className, ...messageBubbleProps }) => ( + <MessageBubble nude className={createClassName(styles, 'image-attachment', {}, [className])} {...messageBubbleProps}> + <img src={url} className={createClassName(styles, 'image-attachment__inner')} /> </MessageBubble> )); diff --git a/packages/livechat/src/components/Messages/ImageAttachment/stories.js b/packages/livechat/src/components/Messages/ImageAttachment/stories.js index a901fb35873..538867d972e 100644 --- a/packages/livechat/src/components/Messages/ImageAttachment/stories.js +++ b/packages/livechat/src/components/Messages/ImageAttachment/stories.js @@ -8,8 +8,4 @@ import { centered } from '../../../helpers.stories'; storiesOf('Messages/ImageAttachment', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('default', () => ( - <ImageAttachment - url={text('url', sampleImage)} - /> - )); + .add('default', () => <ImageAttachment url={text('url', sampleImage)} />); diff --git a/packages/livechat/src/components/Messages/Message/index.js b/packages/livechat/src/components/Messages/Message/index.js index 8226907003a..b2faf1940aa 100644 --- a/packages/livechat/src/components/Messages/Message/index.js +++ b/packages/livechat/src/components/Messages/Message/index.js @@ -28,60 +28,31 @@ import { MESSAGE_WEBRTC_CALL, } from '../constants'; -const renderContent = ({ - text, - system, - quoted, - me, - blocks, - attachments, - attachmentResolver, - mid, - rid, -}) => [ - ...(attachments || []) - .map((attachment) => - (attachment.audio_url - && <AudioAttachment - quoted={quoted} - url={attachmentResolver(attachment.audio_url)} - />) - || (attachment.video_url - && <VideoAttachment - quoted={quoted} - url={attachmentResolver(attachment.video_url)} - />) - || (attachment.image_url - && <ImageAttachment - quoted={quoted} - url={attachmentResolver(attachment.image_url)} - />) - || (attachment.title_link - && <FileAttachment - quoted={quoted} - url={attachmentResolver(attachment.title_link)} - title={attachment.title} - />) - || ((attachment.message_link || attachment.tmid) && renderContent({ - text: attachment.text, - quoted: true, - attachments: attachment.attachments, - attachmentResolver, - })), +const renderContent = ({ text, system, quoted, me, blocks, attachments, attachmentResolver, mid, rid }) => + [ + ...(attachments || []).map( + (attachment) => + (attachment.audio_url && <AudioAttachment quoted={quoted} url={attachmentResolver(attachment.audio_url)} />) || + (attachment.video_url && <VideoAttachment quoted={quoted} url={attachmentResolver(attachment.video_url)} />) || + (attachment.image_url && <ImageAttachment quoted={quoted} url={attachmentResolver(attachment.image_url)} />) || + (attachment.title_link && ( + <FileAttachment quoted={quoted} url={attachmentResolver(attachment.title_link)} title={attachment.title} /> + )) || + ((attachment.message_link || attachment.tmid) && + renderContent({ + text: attachment.text, + quoted: true, + attachments: attachment.attachments, + attachmentResolver, + })), ), - text && ( - <MessageBubble inverse={me} quoted={quoted} system={system}> - <MessageText text={text} system={system} /> - </MessageBubble> - ), - blocks && ( - <MessageBlocks - blocks={blocks} - mid={mid} - rid={rid} - /> - ), -].filter(Boolean); + text && ( + <MessageBubble inverse={me} quoted={quoted} system={system}> + <MessageText text={text} system={system} /> + </MessageBubble> + ), + blocks && <MessageBlocks blocks={blocks} mid={mid} rid={rid} />, + ].filter(Boolean); const resolveWebRTCEndCallMessage = ({ webRtcCallEndTs, ts, t }) => { const callEndTime = resolveDate(webRtcCallEndTs); @@ -91,23 +62,27 @@ const resolveWebRTCEndCallMessage = ({ webRtcCallEndTs, ts, t }) => { return t('call_end_time', { time, callDuration }); }; -const getSystemMessageText = ({ type, conversationFinishedMessage, transferData, u, webRtcCallEndTs, ts }, t) => (type === MESSAGE_TYPE_ROOM_NAME_CHANGED && t('room_name_changed')) - || (type === MESSAGE_TYPE_USER_ADDED && t('user_added_by')) - || (type === MESSAGE_TYPE_USER_REMOVED && t('user_removed_by')) - || (type === MESSAGE_TYPE_USER_JOINED && t('user_joined')) - || (type === MESSAGE_TYPE_USER_LEFT && t('user_left')) - || (type === MESSAGE_TYPE_WELCOME && t('welcome')) - || (type === MESSAGE_TYPE_LIVECHAT_CLOSED && (conversationFinishedMessage || t('conversation_finished'))) - || (type === MESSAGE_TYPE_LIVECHAT_STARTED && t('chat_started')) - || (type === MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY && normalizeTransferHistoryMessage(transferData, u, t)) - || (type === MESSAGE_WEBRTC_CALL && webRtcCallEndTs && ts && resolveWebRTCEndCallMessage({ webRtcCallEndTs, ts, t })); +const getSystemMessageText = ({ type, conversationFinishedMessage, transferData, u, webRtcCallEndTs, ts }, t) => + (type === MESSAGE_TYPE_ROOM_NAME_CHANGED && t('room_name_changed')) || + (type === MESSAGE_TYPE_USER_ADDED && t('user_added_by')) || + (type === MESSAGE_TYPE_USER_REMOVED && t('user_removed_by')) || + (type === MESSAGE_TYPE_USER_JOINED && t('user_joined')) || + (type === MESSAGE_TYPE_USER_LEFT && t('user_left')) || + (type === MESSAGE_TYPE_WELCOME && t('welcome')) || + (type === MESSAGE_TYPE_LIVECHAT_CLOSED && (conversationFinishedMessage || t('conversation_finished'))) || + (type === MESSAGE_TYPE_LIVECHAT_STARTED && t('chat_started')) || + (type === MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY && normalizeTransferHistoryMessage(transferData, u, t)) || + (type === MESSAGE_WEBRTC_CALL && webRtcCallEndTs && ts && resolveWebRTCEndCallMessage({ webRtcCallEndTs, ts, t })); const getMessageUsernames = (compact, message) => { if (compact || !message.u) { return []; } - const { alias, u: { username, name } } = message; + const { + alias, + u: { username, name }, + } = message; if (alias && name) { return [name]; } @@ -115,46 +90,26 @@ const getMessageUsernames = (compact, message) => { return [username]; }; -const Message = memo(({ - avatarResolver, - attachmentResolver = getAttachmentUrl, - use, - me, - compact, - className, - style = {}, - t, - ...message -}) => ( - <MessageContainer - id={message._id} - compact={compact} - reverse={me} - use={use} - className={className} - style={style} - system={!!message.type} - > - {!message.type && <MessageAvatars - avatarResolver={avatarResolver} - usernames={getMessageUsernames(compact, message)} - />} - <MessageContent reverse={me}> - {renderContent({ - text: message.type ? getSystemMessageText(message, t) : message.msg, - system: !!message.type, - me, - attachments: message.attachments, - blocks: message.blocks, - mid: message._id, - rid: message.rid, - attachmentResolver, - })} - </MessageContent> +const Message = memo( + ({ avatarResolver, attachmentResolver = getAttachmentUrl, use, me, compact, className, style = {}, t, ...message }) => ( + <MessageContainer id={message._id} compact={compact} reverse={me} use={use} className={className} style={style} system={!!message.type}> + {!message.type && <MessageAvatars avatarResolver={avatarResolver} usernames={getMessageUsernames(compact, message)} />} + <MessageContent reverse={me}> + {renderContent({ + text: message.type ? getSystemMessageText(message, t) : message.msg, + system: !!message.type, + me, + attachments: message.attachments, + blocks: message.blocks, + mid: message._id, + rid: message.rid, + attachmentResolver, + })} + </MessageContent> - {!compact && !message.type && <MessageTime normal={!me} inverse={me} ts={message.ts} />} - - </MessageContainer> -)); + {!compact && !message.type && <MessageTime normal={!me} inverse={me} ts={message.ts} />} + </MessageContainer> + ), +); export default withTranslation()(Message); diff --git a/packages/livechat/src/components/Messages/Message/stories.js b/packages/livechat/src/components/Messages/Message/stories.js index 2f93854105a..5237af9e032 100644 --- a/packages/livechat/src/components/Messages/Message/stories.js +++ b/packages/livechat/src/components/Messages/Message/stories.js @@ -213,10 +213,12 @@ export const WithQuotation = (args) => ( ); WithQuotation.storyName = 'with quotation'; WithQuotation.args = { - attachments: [{ - message_link: 'http://localhost:3000/live/SqouQyJ7wDsK8KPnc?msg=EWrxmazqYbEf3rFzd', - text: defaultMessageExtra, - }], + attachments: [ + { + message_link: 'http://localhost:3000/live/SqouQyJ7wDsK8KPnc?msg=EWrxmazqYbEf3rFzd', + text: defaultMessageExtra, + }, + ], }; export const WithAudioAttachment = (args) => ( @@ -235,9 +237,11 @@ export const WithAudioAttachment = (args) => ( ); WithAudioAttachment.storyName = 'with audio attachment'; WithAudioAttachment.args = { - attachments: [{ - audio_url: sampleAudio, - }], + attachments: [ + { + audio_url: sampleAudio, + }, + ], }; export const WithVideoAttachment = (args) => ( @@ -256,9 +260,11 @@ export const WithVideoAttachment = (args) => ( ); WithVideoAttachment.storyName = 'with video attachment'; WithVideoAttachment.args = { - attachments: [{ - video_url: sampleVideo, - }], + attachments: [ + { + video_url: sampleVideo, + }, + ], }; export const WithImageAttachment = (args) => ( @@ -277,9 +283,11 @@ export const WithImageAttachment = (args) => ( ); WithImageAttachment.storyName = 'with image attachment'; WithImageAttachment.args = { - attachments: [{ - image_url: sampleImage, - }], + attachments: [ + { + image_url: sampleImage, + }, + ], }; export const WithFilesAttachments = (args) => ( @@ -298,11 +306,10 @@ export const WithFilesAttachments = (args) => ( ); WithFilesAttachments.storyName = 'with files attachments'; WithFilesAttachments.args = { - attachments: ['pdf', 'doc', 'ppt', 'xls', 'zip', 'abc'] - .map((extension) => ({ - title_link: `http://example.com/demo.${ extension }`, - title: `Untitled ${ extension } file`, - })), + attachments: ['pdf', 'doc', 'ppt', 'xls', 'zip', 'abc'].map((extension) => ({ + title_link: `http://example.com/demo.${extension}`, + title: `Untitled ${extension} file`, + })), }; export const WithMultipleAttachments = (args) => ( diff --git a/packages/livechat/src/components/Messages/MessageAvatars/index.js b/packages/livechat/src/components/Messages/MessageAvatars/index.js index 9318591a12f..b80a5971294 100644 --- a/packages/livechat/src/components/Messages/MessageAvatars/index.js +++ b/packages/livechat/src/components/Messages/MessageAvatars/index.js @@ -2,22 +2,10 @@ import { Avatar } from '../../Avatar'; import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; -export const MessageAvatars = memo(({ - avatarResolver = () => null, - usernames = [], - className, - style = {}, -}) => ( - <div - className={createClassName(styles, 'message-avatars', {}, [className])} - style={style} - > +export const MessageAvatars = memo(({ avatarResolver = () => null, usernames = [], className, style = {} }) => ( + <div className={createClassName(styles, 'message-avatars', {}, [className])} style={style}> {usernames.map((username) => ( - <Avatar - src={avatarResolver(username)} - description={username} - className={createClassName(styles, 'message-avatars__avatar')} - /> + <Avatar src={avatarResolver(username)} description={username} className={createClassName(styles, 'message-avatars__avatar')} /> ))} </div> )); diff --git a/packages/livechat/src/components/Messages/MessageAvatars/stories.js b/packages/livechat/src/components/Messages/MessageAvatars/stories.js index 8337fded536..343ac1d4a5e 100644 --- a/packages/livechat/src/components/Messages/MessageAvatars/stories.js +++ b/packages/livechat/src/components/Messages/MessageAvatars/stories.js @@ -4,27 +4,13 @@ import { storiesOf } from '@storybook/react'; import { MessageAvatars } from '.'; import { avatarResolver, centered } from '../../../helpers.stories'; - storiesOf('Messages/MessageAvatars', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('empty', () => ( - <MessageAvatars - avatarResolver={avatarResolver} - usernames={object('usernames', [])} - /> - )) - .add('with one avatar', () => ( - <MessageAvatars - avatarResolver={avatarResolver} - usernames={object('usernames', ['guilherme.gazzo'])} - /> - )) + .add('empty', () => <MessageAvatars avatarResolver={avatarResolver} usernames={object('usernames', [])} />) + .add('with one avatar', () => <MessageAvatars avatarResolver={avatarResolver} usernames={object('usernames', ['guilherme.gazzo'])} />) .add('with two avatars', () => ( - <MessageAvatars - avatarResolver={avatarResolver} - usernames={object('usernames', ['guilherme.gazzo', 'tasso.evangelista'])} - /> + <MessageAvatars avatarResolver={avatarResolver} usernames={object('usernames', ['guilherme.gazzo', 'tasso.evangelista'])} /> )) .add('with three avatars', () => ( <MessageAvatars @@ -33,8 +19,5 @@ storiesOf('Messages/MessageAvatars', module) /> )) .add('with name as avatar instead of username for guests', () => ( - <MessageAvatars - avatarResolver={avatarResolver} - usernames={object('usernames', ['livechat guest'])} - /> + <MessageAvatars avatarResolver={avatarResolver} usernames={object('usernames', ['livechat guest'])} /> )); diff --git a/packages/livechat/src/components/Messages/MessageBlocks/index.js b/packages/livechat/src/components/Messages/MessageBlocks/index.js index 53e701ef748..5078155c2d3 100644 --- a/packages/livechat/src/components/Messages/MessageBlocks/index.js +++ b/packages/livechat/src/components/Messages/MessageBlocks/index.js @@ -1,41 +1,37 @@ import { memo, useCallback } from 'preact/compat'; -import { - triggerAction, - UIKitIncomingInteractionType, - UIKitIncomingInteractionContainerType, -} from '../../../lib/uiKit'; +import { triggerAction, UIKitIncomingInteractionType, UIKitIncomingInteractionContainerType } from '../../../lib/uiKit'; import { createClassName } from '../../helpers'; import { renderMessageBlocks } from '../../uiKit'; import Surface from '../../uiKit/message/Surface'; import styles from './styles.scss'; const MessageBlocks = ({ blocks = [], mid, rid }) => { - const dispatchAction = useCallback(({ - appId, - actionId, - payload, - }) => triggerAction({ - appId, - type: UIKitIncomingInteractionType.BLOCK, - actionId, - rid, - mid, - viewId: null, - container: { - type: UIKitIncomingInteractionContainerType.MESSAGE, - id: mid, - }, - payload, - }), [mid, rid]); + const dispatchAction = useCallback( + ({ appId, actionId, payload }) => + triggerAction({ + appId, + type: UIKitIncomingInteractionType.BLOCK, + actionId, + rid, + mid, + viewId: null, + container: { + type: UIKitIncomingInteractionContainerType.MESSAGE, + id: mid, + }, + payload, + }), + [mid, rid], + ); - return <Surface dispatchAction={dispatchAction}> - {Array.isArray(blocks) && blocks.length > 0 - ? <div className={createClassName(styles, 'message-blocks')}> - {renderMessageBlocks(blocks)} - </div> - : null} - </Surface>; + return ( + <Surface dispatchAction={dispatchAction}> + {Array.isArray(blocks) && blocks.length > 0 ? ( + <div className={createClassName(styles, 'message-blocks')}>{renderMessageBlocks(blocks)}</div> + ) : null} + </Surface> + ); }; export default memo(MessageBlocks); diff --git a/packages/livechat/src/components/Messages/MessageBlocks/stories.js b/packages/livechat/src/components/Messages/MessageBlocks/stories.js index e239b089711..df201dc4c6b 100644 --- a/packages/livechat/src/components/Messages/MessageBlocks/stories.js +++ b/packages/livechat/src/components/Messages/MessageBlocks/stories.js @@ -8,12 +8,10 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [ - (storyFn) => <PopoverContainer children={storyFn()} />, - ], + decorators: [(storyFn) => <PopoverContainer children={storyFn()} />], }; -export const WithBlocks = () => +export const WithBlocks = () => ( <MessageBlocks blocks={[ { @@ -249,9 +247,7 @@ export const WithBlocks = () => emoji: true, }, filter: { - include: [ - 'private', - ], + include: ['private'], }, }, ], @@ -395,5 +391,6 @@ export const WithBlocks = () => ], }, ]} - />; + /> +); WithBlocks.storyName = 'with blocks'; diff --git a/packages/livechat/src/components/Messages/MessageBubble/index.js b/packages/livechat/src/components/Messages/MessageBubble/index.js index 5d3710964ef..1c789c47f88 100644 --- a/packages/livechat/src/components/Messages/MessageBubble/index.js +++ b/packages/livechat/src/components/Messages/MessageBubble/index.js @@ -1,22 +1,8 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - -export const MessageBubble = memo(({ - inverse, - nude, - quoted, - className, - style = {}, - children, - system = false, -}) => ( - <div - className={createClassName(styles, 'message-bubble', { inverse, nude, quoted, system }, [className])} - style={style} - > - <div className={createClassName(styles, 'message-bubble__inner')}> - {children} - </div> +export const MessageBubble = memo(({ inverse, nude, quoted, className, style = {}, children, system = false }) => ( + <div className={createClassName(styles, 'message-bubble', { inverse, nude, quoted, system }, [className])} style={style}> + <div className={createClassName(styles, 'message-bubble__inner')}>{children}</div> </div> )); diff --git a/packages/livechat/src/components/Messages/MessageBubble/stories.js b/packages/livechat/src/components/Messages/MessageBubble/stories.js index 4d59ac48e37..78c59ff97d2 100644 --- a/packages/livechat/src/components/Messages/MessageBubble/stories.js +++ b/packages/livechat/src/components/Messages/MessageBubble/stories.js @@ -4,45 +4,28 @@ import { storiesOf } from '@storybook/react'; import { MessageBubble } from '.'; import { loremIpsum, centered } from '../../../helpers.stories'; - const text = loremIpsum({ count: 1, units: 'sentences' }); storiesOf('Messages/MessageBubble', module) .addDecorator(centered) .addDecorator(withKnobs) .add('default', () => ( - <MessageBubble - inverse={boolean('inverse', false)} - nude={boolean('nude', false)} - quoted={boolean('quoted', false)} - > + <MessageBubble inverse={boolean('inverse', false)} nude={boolean('nude', false)} quoted={boolean('quoted', false)}> {text} </MessageBubble> )) .add('inverse', () => ( - <MessageBubble - inverse={boolean('inverse', true)} - nude={boolean('nude', false)} - quoted={boolean('quoted', false)} - > + <MessageBubble inverse={boolean('inverse', true)} nude={boolean('nude', false)} quoted={boolean('quoted', false)}> {text} </MessageBubble> )) .add('nude', () => ( - <MessageBubble - inverse={boolean('inverse', false)} - nude={boolean('nude', true)} - quoted={boolean('quoted', false)} - > + <MessageBubble inverse={boolean('inverse', false)} nude={boolean('nude', true)} quoted={boolean('quoted', false)}> {text} </MessageBubble> )) .add('quoted', () => ( - <MessageBubble - inverse={boolean('inverse', false)} - nude={boolean('nude', false)} - quoted={boolean('quoted', true)} - > + <MessageBubble inverse={boolean('inverse', false)} nude={boolean('nude', false)} quoted={boolean('quoted', true)}> {text} </MessageBubble> )); diff --git a/packages/livechat/src/components/Messages/MessageContainer/index.js b/packages/livechat/src/components/Messages/MessageContainer/index.js index 097f645917b..c2ee4a9c0a1 100644 --- a/packages/livechat/src/components/Messages/MessageContainer/index.js +++ b/packages/livechat/src/components/Messages/MessageContainer/index.js @@ -1,22 +1,8 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - -export const MessageContainer = memo(({ - id, - compact, - reverse, - use: Element = 'div', - className, - style = {}, - children, - system = false, -}) => ( - <Element - id={id} - className={createClassName(styles, 'message-container', { compact, reverse, system }, [className])} - style={style} - > +export const MessageContainer = memo(({ id, compact, reverse, use: Element = 'div', className, style = {}, children, system = false }) => ( + <Element id={id} className={createClassName(styles, 'message-container', { compact, reverse, system }, [className])} style={style}> {children} </Element> )); diff --git a/packages/livechat/src/components/Messages/MessageContent/index.js b/packages/livechat/src/components/Messages/MessageContent/index.js index 3720068f456..a13a55eeae9 100644 --- a/packages/livechat/src/components/Messages/MessageContent/index.js +++ b/packages/livechat/src/components/Messages/MessageContent/index.js @@ -1,12 +1,8 @@ import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - export const MessageContent = memo(({ reverse, className, style = {}, children }) => ( - <div - className={createClassName(styles, 'message-content', { reverse }, [className])} - style={style} - > + <div className={createClassName(styles, 'message-content', { reverse }, [className])} style={style}> {children} </div> )); diff --git a/packages/livechat/src/components/Messages/MessageList/index.js b/packages/livechat/src/components/Messages/MessageList/index.js index 2b4ecff9358..7ac2dbf60ab 100644 --- a/packages/livechat/src/components/Messages/MessageList/index.js +++ b/packages/livechat/src/components/Messages/MessageList/index.js @@ -100,7 +100,9 @@ export class MessageList extends MemoizedComponent { } isVideoConfMessage(message) { - return Boolean(message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat')); + return Boolean( + message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'), + ); } renderItems = ({ @@ -121,18 +123,22 @@ export class MessageList extends MemoizedComponent { const message = messages[i]; const nextMessage = messages[i + 1]; - if ((message.t === constants.webRTCCallStartedMessageType) - && message.actionLinks && message.actionLinks.length - && ongoingCall && isCallOngoing(ongoingCall.callStatus) - && !message.webRtcCallEndTs) { + if ( + message.t === constants.webRTCCallStartedMessageType && + message.actionLinks && + message.actionLinks.length && + ongoingCall && + isCallOngoing(ongoingCall.callStatus) && + !message.webRtcCallEndTs + ) { const { url, callProvider, rid } = incomingCallAlert || {}; - items.push( - <JoinCallButton callStatus={ongoingCall.callStatus} url={url} callProvider={callProvider} rid={rid} />, - ); + items.push(<JoinCallButton callStatus={ongoingCall.callStatus} url={url} callProvider={callProvider} rid={rid} />); continue; } - const videoConfJoinBlock = message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); + const videoConfJoinBlock = message.blocks + ?.find(({ appId }) => appId === 'videoconf-core') + ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); if (videoConfJoinBlock) { // If the call is not accepted yet, don't render the message. if (!ongoingCall || !isCallOngoing(ongoingCall.callStatus)) { @@ -142,13 +148,7 @@ export class MessageList extends MemoizedComponent { const showDateSeparator = !previousMessage || !isSameDay(parseISO(message.ts), parseISO(previousMessage.ts)); if (showDateSeparator) { - items.push( - <MessageSeparator - key={`sep-${ message.ts }`} - use='li' - date={message.ts} - />, - ); + items.push(<MessageSeparator key={`sep-${message.ts}`} use='li' date={message.ts} />); } items.push( @@ -167,43 +167,25 @@ export class MessageList extends MemoizedComponent { const showUnreadSeparator = lastReadMessageId && nextMessage && lastReadMessageId === message._id; if (showUnreadSeparator) { - items.push( - <MessageSeparator - key='unread' - use='li' - unread - />, - ); + items.push(<MessageSeparator key='unread' use='li' unread />); } } if (typingUsernames && typingUsernames.length) { - items.push( - <TypingIndicator - key='typing' - use='li' - avatarResolver={avatarResolver} - usernames={typingUsernames} - />, - ); + items.push(<TypingIndicator key='typing' use='li' avatarResolver={avatarResolver} usernames={typingUsernames} />); } return items; }; - render = ({ - className, - style = {}, - }) => ( + render = ({ className, style = {} }) => ( <div onScroll={this.handleScroll} className={createClassName(styles, 'message-list', {}, [className])} onClick={this.handleClick} style={style} > - <ol className={createClassName(styles, 'message-list__content')}> - {this.renderItems(this.props)} - </ol> + <ol className={createClassName(styles, 'message-list__content')}>{this.renderItems(this.props)}</ol> </div> ); } diff --git a/packages/livechat/src/components/Messages/MessageList/stories.js b/packages/livechat/src/components/Messages/MessageList/stories.js index b75ba7e2da2..8a8ffee38b3 100644 --- a/packages/livechat/src/components/Messages/MessageList/stories.js +++ b/packages/livechat/src/components/Messages/MessageList/stories.js @@ -4,15 +4,10 @@ import { storiesOf } from '@storybook/react'; import { MessageList } from '.'; import { avatarResolver, loremIpsum, centered } from '../../../helpers.stories'; -import { - MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY, -} from '../constants'; +import { MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY } from '../constants'; -const fittingScreen = (storyFn, ...args) => centered(() => ( - <div style={{ display: 'flex', width: '100vw', height: '100vh' }}> - {storyFn()} - </div> -), ...args); +const fittingScreen = (storyFn, ...args) => + centered(() => <div style={{ display: 'flex', width: '100vw', height: '100vh' }}>{storyFn()}</div>, ...args); const now = new Date(Date.parse('2021-01-01T00:00:00.000Z')); @@ -56,17 +51,20 @@ storiesOf('Messages/MessageList', module) )) .add('with system message', () => ( <MessageList - messages={object('messages', [...messages, { - msg: '', - ts: now.toISOString(), - t: MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY, - transferData: { - transferredBy: users[0], - scope: 'queue', + messages={object('messages', [ + ...messages, + { + msg: '', + ts: now.toISOString(), + t: MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY, + transferData: { + transferredBy: users[0], + scope: 'queue', + }, + u: users[0], + _id: 'AGiTzCjYyaypDxpDm', }, - u: users[0], - _id: 'AGiTzCjYyaypDxpDm', - }])} + ])} uid={number('uid', 1)} avatarResolver={avatarResolver} lastReadMessageId={number('lastReadMessageId', 7)} @@ -76,17 +74,20 @@ storiesOf('Messages/MessageList', module) )) .add('with hidden agent info system message', () => ( <MessageList - messages={object('messages', [...messages, { - msg: '', - t: MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY, - transferData: { - transferredBy: { ...users[0], username: undefined }, - scope: 'queue', + messages={object('messages', [ + ...messages, + { + msg: '', + t: MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY, + transferData: { + transferredBy: { ...users[0], username: undefined }, + scope: 'queue', + }, + ts: now.toISOString(), + u: { ...users[0], username: undefined }, + _id: 'AGiTzCjYyaypDxpDm', }, - ts: now.toISOString(), - u: { ...users[0], username: undefined }, - _id: 'AGiTzCjYyaypDxpDm', - }])} + ])} uid={number('uid', 1)} avatarResolver={avatarResolver} lastReadMessageId={number('lastReadMessageId', 7)} diff --git a/packages/livechat/src/components/Messages/MessageSeparator/index.js b/packages/livechat/src/components/Messages/MessageSeparator/index.js index d94f93e719c..40085afb5ef 100644 --- a/packages/livechat/src/components/Messages/MessageSeparator/index.js +++ b/packages/livechat/src/components/Messages/MessageSeparator/index.js @@ -3,34 +3,30 @@ import { withTranslation } from 'react-i18next'; import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - -const MessageSeparator = memo(({ - date, - unread, - use: Element = 'div', - className, - style = {}, - t, -}) => ( +const MessageSeparator = memo(({ date, unread, use: Element = 'div', className, style = {}, t }) => ( <Element - className={createClassName(styles, 'separator', { - date: !!date && !unread, - unread: !date && !!unread, - }, [className])} + className={createClassName( + styles, + 'separator', + { + date: !!date && !unread, + unread: !date && !!unread, + }, + [className], + )} style={style} > <hr className={createClassName(styles, 'separator__line')} /> {(date || unread) && ( <span className={createClassName(styles, 'separator__text')}> - { - (!!date && t('message_separator_date', { + {(!!date && + t('message_separator_date', { val: new Date(date), formatParams: { val: { month: 'short', day: '2-digit', year: 'numeric' }, }, - }).toUpperCase()) - || (unread && t('unread_messages')) - } + }).toUpperCase()) || + (unread && t('unread_messages'))} </span> )} <hr className={createClassName(styles, 'separator__line')} /> diff --git a/packages/livechat/src/components/Messages/MessageSeparator/stories.js b/packages/livechat/src/components/Messages/MessageSeparator/stories.js index 477d1ffabc9..7a3dac28d07 100644 --- a/packages/livechat/src/components/Messages/MessageSeparator/stories.js +++ b/packages/livechat/src/components/Messages/MessageSeparator/stories.js @@ -4,27 +4,11 @@ import { storiesOf } from '@storybook/react'; import MessageSeparator from '.'; import { centered } from '../../../helpers.stories'; - const defaultDate = new Date(Date.parse('2021-01-01T00:00:00.000Z')); storiesOf('Messages/MessageSeparator', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('default', () => ( - <MessageSeparator - date={null} - unread={boolean('unread', false)} - /> - )) - .add('date', () => ( - <MessageSeparator - date={new Date(date('date', defaultDate)).toISOString()} - unread={boolean('unread', false)} - /> - )) - .add('unread', () => ( - <MessageSeparator - date={null} - unread={boolean('unread', true)} - /> - )); + .add('default', () => <MessageSeparator date={null} unread={boolean('unread', false)} />) + .add('date', () => <MessageSeparator date={new Date(date('date', defaultDate)).toISOString()} unread={boolean('unread', false)} />) + .add('unread', () => <MessageSeparator date={null} unread={boolean('unread', true)} />); diff --git a/packages/livechat/src/components/Messages/MessageText/emoji.js b/packages/livechat/src/components/Messages/MessageText/emoji.js index 9777311b7ed..8cb1d8c54d2 100644 --- a/packages/livechat/src/components/Messages/MessageText/emoji.js +++ b/packages/livechat/src/components/Messages/MessageText/emoji.js @@ -9,9 +9,9 @@ const emojiRanges = [ ].join('|'); // this func is called for each emoji, input-> emoji shortname, o/p-> emoji icon in html format -const transformEmojisToNormalSize = (emoji) => `<span>${ emoji }</span>`; +const transformEmojisToNormalSize = (emoji) => `<span>${emoji}</span>`; -const transformEmojisToLargeSize = (emoji) => `<span style="font-size:2rem">${ emoji }</span>`; +const transformEmojisToLargeSize = (emoji) => `<span style="font-size:2rem">${emoji}</span>`; const removeSpaces = (str) => str && str.replace(/\s/g, ''); diff --git a/packages/livechat/src/components/Messages/MessageText/index.js b/packages/livechat/src/components/Messages/MessageText/index.js index 34712d9da42..00eeb20f387 100644 --- a/packages/livechat/src/components/Messages/MessageText/index.js +++ b/packages/livechat/src/components/Messages/MessageText/index.js @@ -3,12 +3,7 @@ import renderEmojis from './emoji'; import { renderMarkdown } from './markdown'; import styles from './styles.scss'; -export const MessageText = memo(({ - text, - system, - className, - style = {}, -}) => ( +export const MessageText = memo(({ text, system, className, style = {} }) => ( <div // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: renderMarkdown(renderEmojis(text)) }} diff --git a/packages/livechat/src/components/Messages/MessageText/markdown.js b/packages/livechat/src/components/Messages/MessageText/markdown.js index f5b6ee69d24..b614ac8ccf2 100644 --- a/packages/livechat/src/components/Messages/MessageText/markdown.js +++ b/packages/livechat/src/components/Messages/MessageText/markdown.js @@ -1,6 +1,5 @@ import MarkdownIt from 'markdown-it'; - const md = new MarkdownIt({ html: true, breaks: true, @@ -50,7 +49,7 @@ md.use((md) => { return false; } - if (marker !== 0x7E/* ~ */) { + if (marker !== 0x7e /* ~ */) { return false; } diff --git a/packages/livechat/src/components/Messages/MessageTime/index.js b/packages/livechat/src/components/Messages/MessageTime/index.js index bae5f54af85..f599c071b6a 100644 --- a/packages/livechat/src/components/Messages/MessageTime/index.js +++ b/packages/livechat/src/components/Messages/MessageTime/index.js @@ -5,7 +5,6 @@ import { withTranslation } from 'react-i18next'; import { createClassName, memo } from '../../helpers'; import styles from './styles.scss'; - const parseDate = (ts, t) => { const timestamp = new Date(ts).toISOString(); return t('message_time', { diff --git a/packages/livechat/src/components/Messages/MessageTime/stories.js b/packages/livechat/src/components/Messages/MessageTime/stories.js index 009b02cd0a6..84eb51a263f 100644 --- a/packages/livechat/src/components/Messages/MessageTime/stories.js +++ b/packages/livechat/src/components/Messages/MessageTime/stories.js @@ -4,16 +4,11 @@ import { storiesOf } from '@storybook/react'; import MessageTime from '.'; import { centered } from '../../../helpers.stories'; - const today = new Date(Date.parse('2021-01-01T00:00:00.000Z')); const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); storiesOf('Messages/MessageTime', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('today', () => ( - <MessageTime ts={date('ts', today)} /> - )) - .add('yesterday', () => ( - <MessageTime ts={date('ts', yesterday)} /> - )); + .add('today', () => <MessageTime ts={date('ts', today)} />) + .add('yesterday', () => <MessageTime ts={date('ts', yesterday)} />); diff --git a/packages/livechat/src/components/Messages/TypingDots/index.js b/packages/livechat/src/components/Messages/TypingDots/index.js index 269cfa65838..1e5f76b40d3 100644 --- a/packages/livechat/src/components/Messages/TypingDots/index.js +++ b/packages/livechat/src/components/Messages/TypingDots/index.js @@ -1,16 +1,8 @@ import { createClassName } from '../../helpers'; import styles from './styles.scss'; -export const TypingDots = ({ - text, - className, - style = {}, -}) => ( - <div - aria-label={text} - className={createClassName(styles, 'typing-dots', {}, [className])} - style={style} - > +export const TypingDots = ({ text, className, style = {} }) => ( + <div aria-label={text} className={createClassName(styles, 'typing-dots', {}, [className])} style={style}> <span class={createClassName(styles, 'typing-dots__dot')} /> <span class={createClassName(styles, 'typing-dots__dot')} /> <span class={createClassName(styles, 'typing-dots__dot')} /> diff --git a/packages/livechat/src/components/Messages/TypingDots/stories.js b/packages/livechat/src/components/Messages/TypingDots/stories.js index b8d42357328..c006b6ec662 100644 --- a/packages/livechat/src/components/Messages/TypingDots/stories.js +++ b/packages/livechat/src/components/Messages/TypingDots/stories.js @@ -4,10 +4,7 @@ import { storiesOf } from '@storybook/react'; import { TypingDots } from '.'; import { centered } from '../../../helpers.stories'; - storiesOf('Messages/TypingDots', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('default', () => ( - <TypingDots text={text('text', 'The attendant is typing')} /> - )); + .add('default', () => <TypingDots text={text('text', 'The attendant is typing')} />); diff --git a/packages/livechat/src/components/Messages/TypingIndicator/index.js b/packages/livechat/src/components/Messages/TypingIndicator/index.js index 5b73cb3e884..5119d7608f0 100644 --- a/packages/livechat/src/components/Messages/TypingIndicator/index.js +++ b/packages/livechat/src/components/Messages/TypingIndicator/index.js @@ -5,18 +5,9 @@ import { MessageContainer } from '../MessageContainer'; import { MessageContent } from '../MessageContent'; import { TypingDots } from '../TypingDots'; - -export const TypingIndicator = memo(({ - avatarResolver = () => null, - usernames = [], - text, - ...containerProps -}) => ( +export const TypingIndicator = memo(({ avatarResolver = () => null, usernames = [], text, ...containerProps }) => ( <MessageContainer {...containerProps}> - <MessageAvatars - avatarResolver={avatarResolver} - usernames={usernames} - /> + <MessageAvatars avatarResolver={avatarResolver} usernames={usernames} /> <MessageContent> <MessageBubble> <TypingDots text={text} /> diff --git a/packages/livechat/src/components/Messages/TypingIndicator/stories.js b/packages/livechat/src/components/Messages/TypingIndicator/stories.js index 71e4f6a7443..7ee4da241d3 100644 --- a/packages/livechat/src/components/Messages/TypingIndicator/stories.js +++ b/packages/livechat/src/components/Messages/TypingIndicator/stories.js @@ -4,16 +4,11 @@ import { storiesOf } from '@storybook/react'; import { TypingIndicator } from '.'; import { avatarResolver, centered } from '../../../helpers.stories'; - storiesOf('Messages/TypingIndicator', module) .addDecorator(centered) .addDecorator(withKnobs) .add('default', () => ( - <TypingIndicator - avatarResolver={avatarResolver} - usernames={object('usernames', [])} - text={text('text', 'The attendant is typing')} - /> + <TypingIndicator avatarResolver={avatarResolver} usernames={object('usernames', [])} text={text('text', 'The attendant is typing')} /> )) .add('with avatars', () => ( <TypingIndicator diff --git a/packages/livechat/src/components/Messages/VideoAttachment/index.js b/packages/livechat/src/components/Messages/VideoAttachment/index.js index 57fb6ad3e26..be438d53839 100644 --- a/packages/livechat/src/components/Messages/VideoAttachment/index.js +++ b/packages/livechat/src/components/Messages/VideoAttachment/index.js @@ -4,23 +4,9 @@ import { createClassName, memo } from '../../helpers'; import { MessageBubble } from '../MessageBubble'; import styles from './styles.scss'; - -const VideoAttachment = memo(({ - url, - className, - t, - ...messageBubbleProps -}) => ( - <MessageBubble - nude - className={createClassName(styles, 'video-attachment', {}, [className])} - {...messageBubbleProps} - > - <video - src={url} - controls - className={createClassName(styles, 'video-attachment__inner')} - > +const VideoAttachment = memo(({ url, className, t, ...messageBubbleProps }) => ( + <MessageBubble nude className={createClassName(styles, 'video-attachment', {}, [className])} {...messageBubbleProps}> + <video src={url} controls className={createClassName(styles, 'video-attachment__inner')}> {t('you_browser_doesn_t_support_video_element')} </video> </MessageBubble> diff --git a/packages/livechat/src/components/Messages/VideoAttachment/stories.js b/packages/livechat/src/components/Messages/VideoAttachment/stories.js index 3d84e93b74d..c13ae1ef7c4 100644 --- a/packages/livechat/src/components/Messages/VideoAttachment/stories.js +++ b/packages/livechat/src/components/Messages/VideoAttachment/stories.js @@ -5,12 +5,7 @@ import VideoAttachment from '.'; import sampleVideo from '../../../../.storybook/assets/sample-video.mp4'; import { centered } from '../../../helpers.stories'; - storiesOf('Messages/VideoAttachment', module) .addDecorator(centered) .addDecorator(withKnobs) - .add('default', () => ( - <VideoAttachment - url={text('url', sampleVideo)} - /> - )); + .add('default', () => <VideoAttachment url={text('url', sampleVideo)} />); diff --git a/packages/livechat/src/components/Modal/component.js b/packages/livechat/src/components/Modal/component.js index 0e880b359ed..97e97028455 100644 --- a/packages/livechat/src/components/Modal/component.js +++ b/packages/livechat/src/components/Modal/component.js @@ -6,7 +6,6 @@ import { ButtonGroup } from '../ButtonGroup'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - export class Modal extends Component { static defaultProps = { dismissByOverlay: true, @@ -47,56 +46,45 @@ export class Modal extends Component { window.removeEventListener('keydown', this.handleKeyDown, false); } - render = ({ children, animated, open, ...props }) => ( + render = ({ children, animated, open, ...props }) => open ? ( - <div - onTouchStart={this.handleTouchStart} - onMouseDown={this.handleMouseDown} - className={createClassName(styles, 'modal__overlay')} - > - <div className={createClassName(styles, 'modal', { animated })} {...props}>{children}</div> + <div onTouchStart={this.handleTouchStart} onMouseDown={this.handleMouseDown} className={createClassName(styles, 'modal__overlay')}> + <div className={createClassName(styles, 'modal', { animated })} {...props}> + {children} + </div> </div> - ) : null - ); + ) : null; } +export const ModalMessage = ({ children }) => <div className={createClassName(styles, 'modal__message')}>{children}</div>; -export const ModalMessage = ({ children }) => ( - <div className={createClassName(styles, 'modal__message')}> - {children} - </div> -); - - -export const ConfirmationModal = withTranslation()(({ - text, - confirmButtonText, - cancelButtonText, - onConfirm, - onCancel, - t, - ...props -}) => <Modal open animated dismissByOverlay={false} {...props}> - <Modal.Message>{text}</Modal.Message> - <ButtonGroup> - <Button outline secondary onClick={onCancel}>{cancelButtonText || t('no')}</Button> - <Button secondaryDanger onClick={onConfirm}>{confirmButtonText || t('yes')}</Button> - </ButtonGroup> -</Modal>); +export const ConfirmationModal = withTranslation()(({ text, confirmButtonText, cancelButtonText, onConfirm, onCancel, t, ...props }) => ( + <Modal open animated dismissByOverlay={false} {...props}> + <Modal.Message>{text}</Modal.Message> + <ButtonGroup> + <Button outline secondary onClick={onCancel}> + {cancelButtonText || t('no')} + </Button> + <Button secondaryDanger onClick={onConfirm}> + {confirmButtonText || t('yes')} + </Button> + </ButtonGroup> + </Modal> +)); export const AlertModal = withTranslation()(({ text, buttonText, onConfirm, t, ...props }) => ( <Modal open animated dismissByOverlay={false} {...props}> <Modal.Message>{text}</Modal.Message> <ButtonGroup> - <Button secondary onClick={onConfirm}>{buttonText || t('ok')}</Button> + <Button secondary onClick={onConfirm}> + {buttonText || t('ok')} + </Button> </ButtonGroup> </Modal> )); - Modal.Message = ModalMessage; Modal.Confirm = ConfirmationModal; Modal.Alert = AlertModal; - export default Modal; diff --git a/packages/livechat/src/components/Modal/manager.js b/packages/livechat/src/components/Modal/manager.js index 294e4e0a286..c9a31cb5201 100644 --- a/packages/livechat/src/components/Modal/manager.js +++ b/packages/livechat/src/components/Modal/manager.js @@ -1,7 +1,6 @@ import store from '../../store'; import Modal from './component'; - export default { confirm(props = {}) { return new Promise((resolve) => { diff --git a/packages/livechat/src/components/Modal/stories.js b/packages/livechat/src/components/Modal/stories.js index a9bbbd4a475..4bb52a71de9 100644 --- a/packages/livechat/src/components/Modal/stories.js +++ b/packages/livechat/src/components/Modal/stories.js @@ -5,14 +5,16 @@ import { storiesOf } from '@storybook/react'; import { loremIpsum, centered } from '../../helpers.stories'; import Modal from './component'; - const LoremIpsum = ({ padding = '5rem', count = 5, units = 'paragraphs', ...options }) => ( <div style={{ padding }}> - {loremIpsum({ count, units, ...options }).split('\n').map((paragraph) => <p>{paragraph}</p>)} + {loremIpsum({ count, units, ...options }) + .split('\n') + .map((paragraph) => ( + <p>{paragraph}</p> + ))} </div> ); - storiesOf('Components/Modal', module) .addDecorator(centered) .addDecorator(withKnobs) @@ -48,12 +50,7 @@ storiesOf('Components/Modal', module) .add('disallow dismiss by overlay', () => ( <div> <LoremIpsum /> - <Modal - open={boolean('open', true)} - animated={boolean('animated', false)} - dismissByOverlay={false} - onDismiss={action('dismiss')} - > + <Modal open={boolean('open', true)} animated={boolean('animated', false)} dismissByOverlay={false} onDismiss={action('dismiss')}> {text('content', loremIpsum({ count: 1, units: 'paragraphs' }))} </Modal> </div> @@ -73,10 +70,6 @@ storiesOf('Components/Modal', module) .add('alert', () => ( <div> <LoremIpsum /> - <Modal.Alert - text={text('text', 'You look great today.')} - buttonText={text('buttonText', 'OK')} - onConfirm={action('confirm')} - /> + <Modal.Alert text={text('text', 'You look great today.')} buttonText={text('buttonText', 'OK')} onConfirm={action('confirm')} /> </div> )); diff --git a/packages/livechat/src/components/Popover/index.js b/packages/livechat/src/components/Popover/index.js index 17635d8245b..55ed2147acc 100644 --- a/packages/livechat/src/components/Popover/index.js +++ b/packages/livechat/src/components/Popover/index.js @@ -3,20 +3,14 @@ import { Component, createContext } from 'preact'; import { createClassName, normalizeDOMRect } from '../helpers'; import styles from './styles.scss'; - const PopoverContext = createContext(); - const PopoverOverlay = ({ children, className, visible, ...props }) => ( - <div - className={createClassName(styles, 'popover__overlay', { visible }, [className])} - {...props} - > + <div className={createClassName(styles, 'popover__overlay', { visible }, [className])} {...props}> {children} </div> ); - export class PopoverContainer extends Component { state = { renderer: null, @@ -89,9 +83,6 @@ export class PopoverContainer extends Component { ); } - export const PopoverTrigger = ({ children, ...props }) => ( - <PopoverContext.Consumer> - {({ open }) => children[0]({ pop: open.bind(null, children[1], props) })} - </PopoverContext.Consumer> + <PopoverContext.Consumer>{({ open }) => children[0]({ pop: open.bind(null, children[1], props) })}</PopoverContext.Consumer> ); diff --git a/packages/livechat/src/components/Popover/stories.js b/packages/livechat/src/components/Popover/stories.js index bdb478e416f..f7c9ae875a0 100644 --- a/packages/livechat/src/components/Popover/stories.js +++ b/packages/livechat/src/components/Popover/stories.js @@ -5,12 +5,9 @@ import { PopoverContainer, PopoverTrigger } from '.'; import { centered } from '../../helpers.stories'; import { Button } from '../Button'; - const centeredWithPopoverContainer = (storyFn, ...args) => ( <div style={{ display: 'flex', width: '100vw', height: '100vh' }}> - <PopoverContainer> - {centered(storyFn, ...args)} - </PopoverContainer> + <PopoverContainer>{centered(storyFn, ...args)}</PopoverContainer> </div> ); @@ -19,29 +16,21 @@ storiesOf('Components/Popover', module) .addDecorator(centeredWithPopoverContainer) .add('arbitrary renderer', () => ( <PopoverTrigger> - {({ pop }) => ( - <Button onClick={pop}>More options...</Button> - )} + {({ pop }) => <Button onClick={pop}>More options...</Button>} {({ dismiss, triggerBounds = {} }) => ( - <Button - style={{ position: 'absolute', left: triggerBounds.right, top: triggerBounds.bottom }} - outline - onClick={dismiss} - >Close me</Button> + <Button style={{ position: 'absolute', left: triggerBounds.right, top: triggerBounds.bottom }} outline onClick={dismiss}> + Close me + </Button> )} </PopoverTrigger> )) .add('with overlay props', () => ( <PopoverTrigger overlayProps={object('overlayProps', { style: { backgroundColor: 'rgba(0, 0, 0, 0.5)' } })}> - {({ pop }) => ( - <Button onClick={pop}>More options...</Button> - )} + {({ pop }) => <Button onClick={pop}>More options...</Button>} {({ dismiss, triggerBounds = {} }) => ( - <Button - style={{ position: 'absolute', left: triggerBounds.right, top: triggerBounds.bottom }} - outline - onClick={dismiss} - >Close me</Button> + <Button style={{ position: 'absolute', left: triggerBounds.right, top: triggerBounds.bottom }} outline onClick={dismiss}> + Close me + </Button> )} </PopoverTrigger> )); diff --git a/packages/livechat/src/components/Screen/Header.js b/packages/livechat/src/components/Screen/Header.js index 53f22618fe7..793a5a37f6d 100644 --- a/packages/livechat/src/components/Screen/Header.js +++ b/packages/livechat/src/components/Screen/Header.js @@ -49,7 +49,12 @@ class ScreenHeader extends Component { ref={this.handleRef} post={ <Header.Post> - {alerts && alerts.map((alert) => <Alert {...alert} onDismiss={onDismissAlert}>{alert.children}</Alert>)} + {alerts && + alerts.map((alert) => ( + <Alert {...alert} onDismiss={onDismissAlert}> + {alert.children} + </Alert> + ))} </Header.Post> } large={this.largeHeader()} @@ -68,12 +73,8 @@ class ScreenHeader extends Component { <Header.Content> <Header.Title>{this.headerTitle(t)}</Header.Title> - {agent && agent.email && ( - <Header.SubTitle>{agent.email}</Header.SubTitle> - )} - {agent && agent.phone && ( - <Header.CustomField>{agent.phone}</Header.CustomField> - )} + {agent && agent.email && <Header.SubTitle>{agent.email}</Header.SubTitle>} + {agent && agent.phone && <Header.CustomField>{agent.phone}</Header.CustomField>} </Header.Content> <Tooltip.Container> <Header.Actions> @@ -82,26 +83,21 @@ class ScreenHeader extends Component { aria-label={notificationsEnabled ? t('disable_notifications') : t('enable_notifications')} onClick={notificationsEnabled ? onDisableNotifications : onEnableNotifications} > - {notificationsEnabled - ? <NotificationsEnabledIcon width={20} height={20} /> - : <NotificationsDisabledIcon width={20} height={20} /> - } + {notificationsEnabled ? ( + <NotificationsEnabledIcon width={20} height={20} /> + ) : ( + <NotificationsDisabledIcon width={20} height={20} /> + )} </Header.Action> </Tooltip.Trigger> {(expanded || !windowed) && ( <Tooltip.Trigger content={minimized ? t('restore_chat') : t('minimize_chat')}> - <Header.Action - aria-label={minimized ? t('restore_chat') : t('minimize_chat')} - onClick={minimized ? onRestore : onMinimize} - > - {minimized - ? <RestoreIcon width={20} height={20} /> - : <MinimizeIcon width={20} height={20} /> - } + <Header.Action aria-label={minimized ? t('restore_chat') : t('minimize_chat')} onClick={minimized ? onRestore : onMinimize}> + {minimized ? <RestoreIcon width={20} height={20} /> : <MinimizeIcon width={20} height={20} />} </Header.Action> </Tooltip.Trigger> )} - {(!expanded && !windowed) && ( + {!expanded && !windowed && ( <Tooltip.Trigger content={t('expand_chat')} placement='bottom-left'> <Header.Action aria-label={t('expand_chat')} onClick={onOpenWindow}> <OpenWindowIcon width={20} height={20} /> diff --git a/packages/livechat/src/components/Screen/index.js b/packages/livechat/src/components/Screen/index.js index 1f8a104e3ca..c7116b27349 100644 --- a/packages/livechat/src/components/Screen/index.js +++ b/packages/livechat/src/components/Screen/index.js @@ -11,19 +11,12 @@ import ScreenHeader from './Header'; import styles from './styles.scss'; export const ScreenContent = ({ children, nopadding, triggered = false }) => ( - <main className={createClassName(styles, 'screen__main', { nopadding, triggered })}> - {children} - </main> + <main className={createClassName(styles, 'screen__main', { nopadding, triggered })}>{children}</main> ); - export const ScreenFooter = ({ children, options, limit }) => ( <Footer> - {children && ( - <FooterContent> - {children} - </FooterContent> - )} + {children && <FooterContent>{children}</FooterContent>} <FooterContent> {options} {limit} @@ -32,14 +25,7 @@ export const ScreenFooter = ({ children, options, limit }) => ( </Footer> ); -const ChatButton = ({ - text, - minimized, - badge, - onClick, - triggered = false, - agent, -}) => ( +const ChatButton = ({ text, minimized, badge, onClick, triggered = false, agent }) => ( <Button icon={minimized || triggered ? <ChatIcon /> : <CloseIcon />} badge={badge} @@ -75,13 +61,15 @@ const CssVar = ({ theme }) => { }; }, [theme]); - return <style>{` - .${ styles.screen } { - ${ theme.color ? `--color: ${ theme.color };` : '' } - ${ theme.fontColor ? `--font-color: ${ theme.fontColor };` : '' } - ${ theme.iconColor ? `--icon-color: ${ theme.iconColor };` : '' } + return ( + <style>{` + .${styles.screen} { + ${theme.color ? `--color: ${theme.color};` : ''} + ${theme.fontColor ? `--font-color: ${theme.fontColor};` : ''} + ${theme.iconColor ? `--icon-color: ${theme.iconColor};` : ''} } - `}</style>; + `}</style> + ); }; export const Screen = ({ @@ -111,25 +99,31 @@ export const Screen = ({ }) => ( <div className={createClassName(styles, 'screen', { minimized, expanded, windowed, triggered })}> <CssVar theme={theme} /> - {triggered && <Button onClick={onMinimize} className={createClassName(styles, 'screen__chat-close-button')} icon={<CloseIcon />}>Close</Button>} + {triggered && ( + <Button onClick={onMinimize} className={createClassName(styles, 'screen__chat-close-button')} icon={<CloseIcon />}> + Close + </Button> + )} <div className={createClassName(styles, 'screen__inner', { fitTextSize: triggered }, [className])}> <PopoverContainer> - {!triggered && <ScreenHeader - alerts={alerts} - agent={agent} - title={title} - notificationsEnabled={notificationsEnabled} - minimized={minimized} - expanded={expanded} - windowed={windowed} - onDismissAlert={onDismissAlert} - onEnableNotifications={onEnableNotifications} - onDisableNotifications={onDisableNotifications} - onMinimize={onMinimize} - onRestore={onRestore} - onOpenWindow={onOpenWindow} - queueInfo={queueInfo} - />} + {!triggered && ( + <ScreenHeader + alerts={alerts} + agent={agent} + title={title} + notificationsEnabled={notificationsEnabled} + minimized={minimized} + expanded={expanded} + windowed={windowed} + onDismissAlert={onDismissAlert} + onEnableNotifications={onEnableNotifications} + onDisableNotifications={onDisableNotifications} + onMinimize={onMinimize} + onRestore={onRestore} + onOpenWindow={onOpenWindow} + queueInfo={queueInfo} + /> + )} {modal} {children} @@ -149,9 +143,7 @@ export const Screen = ({ </div> ); - Screen.Content = ScreenContent; Screen.Footer = ScreenFooter; - export default Screen; diff --git a/packages/livechat/src/components/Screen/stories.js b/packages/livechat/src/components/Screen/stories.js index 819c2688e5d..3590b00c687 100644 --- a/packages/livechat/src/components/Screen/stories.js +++ b/packages/livechat/src/components/Screen/stories.js @@ -8,7 +8,6 @@ import { screenCentered, gazzoAvatar } from '../../helpers.stories'; import { FooterOptions } from '../Footer'; import Menu from '../Menu'; - const alerts = [ { id: 1, children: 'Success alert', success: true }, { id: 2, children: 'Warning alert', warning: true, timeout: 0 }, @@ -37,9 +36,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('minimized', () => ( @@ -60,9 +57,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('expanded', () => ( @@ -83,9 +78,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('windowed', () => ( @@ -106,9 +99,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('with agent (email)', () => ( @@ -138,9 +129,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('with agent (phone)', () => ( @@ -170,9 +159,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('with agent', () => ( @@ -203,9 +190,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('with hidden agent', () => ( @@ -227,9 +212,7 @@ storiesOf('Components/Screen', module) onRestore={action('restore')} onOpenWindow={action('openWindow')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )) .add('with multiple alerts', () => ( @@ -252,9 +235,7 @@ storiesOf('Components/Screen', module) onOpenWindow={action('openWindow')} onDismissAlert={action('dismiss alert')} > - <Screen.Content> - {text('content', 'Content')} - </Screen.Content> + <Screen.Content>{text('content', 'Content')}</Screen.Content> </Screen> )); storiesOf('Components/Screen/Footer', module) @@ -301,9 +282,7 @@ storiesOf('Components/Screen/Footer', module) onOpenWindow={action('openWindow')} > <Screen.Content /> - <Screen.Footer> - {text('content', 'Lorem ipsum dolor sit amet, his id atqui repudiare.')} - </Screen.Footer> + <Screen.Footer>{text('content', 'Lorem ipsum dolor sit amet, his id atqui repudiare.')}</Screen.Footer> </Screen> )) .add('with options', () => ( @@ -331,7 +310,9 @@ storiesOf('Components/Screen/Footer', module) <Menu.Group> <Menu.Item onClick={action('changeDepartment')}>{i18next.t('change_department')}</Menu.Item> <Menu.Item onClick={action('removeUserData')}>{i18next.t('forget_remove_my_data')}</Menu.Item> - <Menu.Item danger onClick={action('finishChat')}>{i18next.t('finish_this_chat')}</Menu.Item> + <Menu.Item danger onClick={action('finishChat')}> + {i18next.t('finish_this_chat')} + </Menu.Item> </Menu.Group> </FooterOptions> } diff --git a/packages/livechat/src/components/Sound/index.js b/packages/livechat/src/components/Sound/index.js index 9e5dbb07116..6e3eb757192 100644 --- a/packages/livechat/src/components/Sound/index.js +++ b/packages/livechat/src/components/Sound/index.js @@ -31,13 +31,5 @@ export class Sound extends Component { this.handlePlayProp(); } - render = ({ src, onStart, onStop }) => ( - <audio - ref={this.handleRef} - src={src} - onPlay={onStart} - onEnded={onStop} - type='audio/mpeg' - /> - ); + render = ({ src, onStart, onStop }) => <audio ref={this.handleRef} src={src} onPlay={onStart} onEnded={onStop} type='audio/mpeg' />; } diff --git a/packages/livechat/src/components/Sound/stories.js b/packages/livechat/src/components/Sound/stories.js index a26259b1bfb..91c58950afb 100644 --- a/packages/livechat/src/components/Sound/stories.js +++ b/packages/livechat/src/components/Sound/stories.js @@ -7,23 +7,12 @@ import beepAudio from '../../../.storybook/assets/beep.mp3'; import sampleAudio from '../../../.storybook/assets/sample-audio.mp3'; import { centered } from '../../helpers.stories'; - storiesOf('Components/Sound', module) .addDecorator(centered) .addDecorator(withKnobs) .add('short', () => ( - <Sound - src={text('src', beepAudio)} - play={boolean('play', false)} - onStart={action('start')} - onStop={action('stop')} - /> + <Sound src={text('src', beepAudio)} play={boolean('play', false)} onStart={action('start')} onStop={action('stop')} /> )) .add('long', () => ( - <Sound - src={text('src', sampleAudio)} - play={boolean('play', false)} - onStart={action('start')} - onStop={action('stop')} - /> + <Sound src={text('src', sampleAudio)} play={boolean('play', false)} onStart={action('start')} onStop={action('stop')} /> )); diff --git a/packages/livechat/src/components/Tooltip/index.js b/packages/livechat/src/components/Tooltip/index.js index 8b101782090..2b2493519c0 100644 --- a/packages/livechat/src/components/Tooltip/index.js +++ b/packages/livechat/src/components/Tooltip/index.js @@ -3,27 +3,26 @@ import { cloneElement, Component, createContext, toChildArray } from 'preact'; import { createClassName } from '../helpers'; import styles from './styles.scss'; - const getPositioningStyle = (placement, { left, top, right, bottom }) => { switch (placement) { case 'left': return { - left: `${ left }px`, - top: `${ (top + bottom) / 2 }px`, + left: `${left}px`, + top: `${(top + bottom) / 2}px`, }; case 'top': case 'top-left': case 'top-right': return { - left: `${ (left + right) / 2 }px`, - top: `${ top }px`, + left: `${(left + right) / 2}px`, + top: `${top}px`, }; case 'right': return { - left: `${ right }px`, - top: `${ (top + bottom) / 2 }px`, + left: `${right}px`, + top: `${(top + bottom) / 2}px`, }; case 'bottom': @@ -31,13 +30,12 @@ const getPositioningStyle = (placement, { left, top, right, bottom }) => { case 'bottom-right': default: return { - left: `${ (left + right) / 2 }px`, - top: `${ bottom }px`, + left: `${(left + right) / 2}px`, + top: `${bottom}px`, }; } }; - export const Tooltip = ({ children, hidden = false, placement, floating = false, triggerBounds, ...props }) => ( <div className={createClassName(styles, 'tooltip', { hidden, placement, floating })} @@ -48,10 +46,8 @@ export const Tooltip = ({ children, hidden = false, placement, floating = false, </div> ); - const TooltipContext = createContext(); - export class TooltipContainer extends Component { state = { tooltip: null, @@ -62,7 +58,17 @@ export class TooltipContainer extends Component { showTooltip = (event, { content, placement = 'bottom', childIndex }) => { const triggerBounds = event.target.getBoundingClientRect(); - this.setState({ tooltip: <Tooltip floating placement={placement} triggerBounds={triggerBounds}>{content}</Tooltip>, activeChild: childIndex, event, placement, content }); + this.setState({ + tooltip: ( + <Tooltip floating placement={placement} triggerBounds={triggerBounds}> + {content} + </Tooltip> + ), + activeChild: childIndex, + event, + placement, + content, + }); }; hideTooltip = () => { @@ -73,7 +79,11 @@ export class TooltipContainer extends Component { if (this.state.tooltip) { const activeChildren = props?.children?.props?.children[this.state.activeChild]; if (activeChildren && activeChildren.props.content !== this.state.content) { - this.showTooltip(this.state.event, { content: activeChildren.props.content, placement: this.state.placement, childIndex: this.state.activeChild }); + this.showTooltip(this.state.event, { + content: activeChildren.props.content, + placement: this.state.placement, + childIndex: this.state.activeChild, + }); } } } @@ -82,42 +92,36 @@ export class TooltipContainer extends Component { return ( <TooltipContext.Provider value={{ ...this.state, showTooltip: this.showTooltip, hideTooltip: this.hideTooltip }}> {children} - <TooltipContext.Consumer> - {({ tooltip }) => tooltip} - </TooltipContext.Consumer> + <TooltipContext.Consumer>{({ tooltip }) => tooltip}</TooltipContext.Consumer> </TooltipContext.Provider> ); } } - export const TooltipTrigger = ({ children, content, placement }) => ( <TooltipContext.Consumer> - {({ showTooltip, hideTooltip }) => toChildArray(children).map((child, index) => cloneElement(child, { - onMouseEnter: (event) => showTooltip(event, { content, placement, childIndex: index }), - onMouseLeave: (event) => hideTooltip(event), - onFocusCapture: (event) => showTooltip(event, { content, placement, childIndex: index }), - onBlurCapture: (event) => hideTooltip(event), - content, - }))} + {({ showTooltip, hideTooltip }) => + toChildArray(children).map((child, index) => + cloneElement(child, { + onMouseEnter: (event) => showTooltip(event, { content, placement, childIndex: index }), + onMouseLeave: (event) => hideTooltip(event), + onFocusCapture: (event) => showTooltip(event, { content, placement, childIndex: index }), + onBlurCapture: (event) => hideTooltip(event), + content, + }), + ) + } </TooltipContext.Consumer> ); - export const withTooltip = (component) => { - const TooltipConnection = ({ tooltip, ...props }) => ( - <Tooltip.Trigger content={tooltip}> - {component(props)} - </Tooltip.Trigger> - ); - TooltipConnection.displayName = `withTooltip(${ component.displayName })`; + const TooltipConnection = ({ tooltip, ...props }) => <Tooltip.Trigger content={tooltip}>{component(props)}</Tooltip.Trigger>; + TooltipConnection.displayName = `withTooltip(${component.displayName})`; return TooltipConnection; }; - Tooltip.Container = TooltipContainer; Tooltip.Trigger = TooltipTrigger; - export default Tooltip; diff --git a/packages/livechat/src/components/Tooltip/stories.js b/packages/livechat/src/components/Tooltip/stories.js index 8ed491fb6c9..d2acf87b50f 100644 --- a/packages/livechat/src/components/Tooltip/stories.js +++ b/packages/livechat/src/components/Tooltip/stories.js @@ -13,17 +13,16 @@ storiesOf('Components/Tooltip', module) .addDecorator(centered) .addDecorator(withKnobs) .add('inline', () => ( - <Tooltip - hidden={boolean('hidden', tooltipHidden)} - placement={select('placement', placements)} - > + <Tooltip hidden={boolean('hidden', tooltipHidden)} placement={select('placement', placements)}> {text('text', tooltipText)} </Tooltip> )) .add('placements', () => ( <div style={{ display: 'flex', flexDirection: 'column' }}> {placements.map((placement) => ( - <Tooltip hidden={boolean('hidden', tooltipHidden)} placement={placement}>{text('text', tooltipText)}</Tooltip> + <Tooltip hidden={boolean('hidden', tooltipHidden)} placement={placement}> + {text('text', tooltipText)} + </Tooltip> ))} </div> )) diff --git a/packages/livechat/src/components/helpers.js b/packages/livechat/src/components/helpers.js index 075a3a17f1a..42a94fc162d 100644 --- a/packages/livechat/src/components/helpers.js +++ b/packages/livechat/src/components/helpers.js @@ -19,12 +19,15 @@ export function flatMap(arr, mapFunc) { return result; } -export const createClassName = (styles, elementName, modifiers = {}, classes = []) => [ - styles[elementName], - ...flatMap(Object.entries(modifiers), ([modifierKey, modifierValue]) => [ - modifierValue && styles[`${ elementName }--${ modifierKey }`], - typeof modifierValue !== 'boolean' && styles[`${ elementName }--${ modifierKey }-${ modifierValue }`], - ]).filter((className) => !!className), ...classes.filter((className) => !!className)].join(' '); +export const createClassName = (styles, elementName, modifiers = {}, classes = []) => + [ + styles[elementName], + ...flatMap(Object.entries(modifiers), ([modifierKey, modifierValue]) => [ + modifierValue && styles[`${elementName}--${modifierKey}`], + typeof modifierValue !== 'boolean' && styles[`${elementName}--${modifierKey}-${modifierValue}`], + ]).filter((className) => !!className), + ...classes.filter((className) => !!className), + ].join(' '); export async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { @@ -36,7 +39,7 @@ export async function asyncForEach(array, callback) { export async function asyncEvery(array, callback) { for (let index = 0; index < array.length; index++) { // eslint-disable-next-line no-await-in-loop - if (!await callback(array[index], index, array)) { + if (!(await callback(array[index], index, array))) { return false; } } @@ -60,7 +63,7 @@ export const debounce = (func, delay) => { export const throttle = (func, limit) => { let inThrottle; - return function(...args) { + return function (...args) { const context = this; if (!inThrottle) { func.apply(context, args); @@ -110,31 +113,31 @@ export function upsert(array = [], item, predicate, ranking) { const getSecureCookieSettings = () => (useSsl ? 'SameSite=None; Secure;' : ''); export const setInitCookies = () => { - document.cookie = `rc_is_widget=t; path=/; ${ getSecureCookieSettings() }`; - document.cookie = `rc_room_type=l; path=/; ${ getSecureCookieSettings() }`; + document.cookie = `rc_is_widget=t; path=/; ${getSecureCookieSettings()}`; + document.cookie = `rc_room_type=l; path=/; ${getSecureCookieSettings()}`; }; export const setCookies = (rid, token) => { - document.cookie = `rc_rid=${ rid }; path=/; ${ getSecureCookieSettings() }`; - document.cookie = `rc_token=${ token }; path=/; ${ getSecureCookieSettings() }`; - document.cookie = `rc_room_type=l; path=/; ${ getSecureCookieSettings() }`; + document.cookie = `rc_rid=${rid}; path=/; ${getSecureCookieSettings()}`; + document.cookie = `rc_token=${token}; path=/; ${getSecureCookieSettings()}`; + document.cookie = `rc_room_type=l; path=/; ${getSecureCookieSettings()}`; }; -export const getAvatarUrl = (username) => (username ? `${ Livechat.client.host }/avatar/${ username }` : null); +export const getAvatarUrl = (username) => (username ? `${Livechat.client.host}/avatar/${username}` : null); export const msgTypesNotRendered = ['livechat_video_call', 'livechat_navigation_history', 'au', 'command', 'uj', 'ul', 'livechat-close']; export const canRenderMessage = ({ t }) => !msgTypesNotRendered.includes(t); -export const getAttachmentUrl = (url) => `${ Livechat.client.host }${ url }`; - -export const sortArrayByColumn = (array, column, inverted) => array.sort((a, b) => { - if (a[column] < b[column] && !inverted) { - return -1; - } - return 1; -}); +export const getAttachmentUrl = (url) => `${Livechat.client.host}${url}`; +export const sortArrayByColumn = (array, column, inverted) => + array.sort((a, b) => { + if (a[column] < b[column] && !inverted) { + return -1; + } + return 1; + }); export const normalizeTransferHistoryMessage = (transferData, sender, t) => { if (!transferData) { @@ -216,7 +219,6 @@ export const visibility = (() => { }; })(); - export class MemoizedComponent extends Component { shouldComponentUpdate(nextProps) { const { props } = this; @@ -273,17 +275,12 @@ const escapeMap = { '<': '<', '>': '>', '"': '"', - '\'': ''', + "'": ''', '`': '`', }; -const escapeRegex = new RegExp(`(?:${ Object.keys(escapeMap).join('|') })`, 'g'); +const escapeRegex = new RegExp(`(?:${Object.keys(escapeMap).join('|')})`, 'g'); -const escapeHtml = mem( - (string) => string.replace(escapeRegex, (match) => escapeMap[match]), -); +const escapeHtml = mem((string) => string.replace(escapeRegex, (match) => escapeMap[match])); -export const parse = (plainText) => - [{ plain: plainText }] - .map(({ plain, html }) => (plain ? escapeHtml(plain) : html || '')) - .join(''); +export const parse = (plainText) => [{ plain: plainText }].map(({ plain, html }) => (plain ? escapeHtml(plain) : html || '')).join(''); diff --git a/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js b/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js index d7eecc4771e..c8c7fe66f5a 100644 --- a/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js @@ -17,23 +17,29 @@ const ActionsBlock = ({ appId, blockId, elements, parser, t }) => { setCollapsed(false); }, []); - return <Block appId={appId} blockId={blockId}> - <div className={createClassName(styles, 'uikit-actions-block')}> - {renderableElements.map((element, key) => { - const renderedElement = parser.renderActions(element, BLOCK_CONTEXT.ACTION); - if (!renderedElement) { - return null; - } + return ( + <Block appId={appId} blockId={blockId}> + <div className={createClassName(styles, 'uikit-actions-block')}> + {renderableElements.map((element, key) => { + const renderedElement = parser.renderActions(element, BLOCK_CONTEXT.ACTION); + if (!renderedElement) { + return null; + } - return <div key={key} className={createClassName(styles, 'uikit-actions-block__item')}> - {renderedElement} - </div>; - })} - {isMoreButtonVisible && <Button outline secondary small onClick={handleMoreButtonClick}> - {t('hiddenelementscount_more', { hiddenElementsCount })} - </Button>} - </div> - </Block>; + return ( + <div key={key} className={createClassName(styles, 'uikit-actions-block__item')}> + {renderedElement} + </div> + ); + })} + {isMoreButtonVisible && ( + <Button outline secondary small onClick={handleMoreButtonClick}> + {t('hiddenelementscount_more', { hiddenElementsCount })} + </Button> + )} + </div> + </Block> + ); }; export default withTranslation()(ActionsBlock); diff --git a/packages/livechat/src/components/uiKit/message/Block.js b/packages/livechat/src/components/uiKit/message/Block.js index 423f37a0476..b90beac1ef5 100644 --- a/packages/livechat/src/components/uiKit/message/Block.js +++ b/packages/livechat/src/components/uiKit/message/Block.js @@ -8,14 +8,15 @@ const BlockContext = createContext({ blockId: null, }); -const Block = ({ appId, blockId, children }) => +const Block = ({ appId, blockId, children }) => ( <BlockContext.Provider children={children} value={{ appId, blockId, }} - />; + /> +); export const usePerformAction = (actionId) => { const { appId } = useContext(BlockContext); @@ -24,25 +25,31 @@ export const usePerformAction = (actionId) => { const [performing, setPerforming] = useState(false); const mountedRef = useRef(true); - useEffect(() => () => { - mountedRef.current = false; - }, []); - - const perform = useCallback(async (payload = {}) => { - setPerforming(true); - - try { - await dispatchAction({ - appId, - actionId, - payload, - }); - } finally { - if (mountedRef.current) { - setPerforming(false); + useEffect( + () => () => { + mountedRef.current = false; + }, + [], + ); + + const perform = useCallback( + async (payload = {}) => { + setPerforming(true); + + try { + await dispatchAction({ + appId, + actionId, + payload, + }); + } finally { + if (mountedRef.current) { + setPerforming(false); + } } - } - }, [actionId, appId, dispatchAction]); + }, + [actionId, appId, dispatchAction], + ); return [perform, performing]; }; diff --git a/packages/livechat/src/components/uiKit/message/ButtonElement/index.js b/packages/livechat/src/components/uiKit/message/ButtonElement/index.js index 397a40e7d6c..663ffac506f 100644 --- a/packages/livechat/src/components/uiKit/message/ButtonElement/index.js +++ b/packages/livechat/src/components/uiKit/message/ButtonElement/index.js @@ -10,35 +10,40 @@ const handleMouseUp = ({ target }) => target.blur(); const ButtonElement = ({ text, actionId, url, value, style, context, confirm, parser }) => { const [performAction, performingAction] = usePerformAction(actionId); - const handleClick = useCallback(async (event) => { - event.preventDefault(); - - if (confirm) { - // TODO - } - - if (url) { - const newTab = window.open(); - newTab.opener = null; - newTab.location = url; - return; - } - - await performAction({ value }); - }, [confirm, performAction, url, value]); - - return <button - children={parser.text(text)} - className={createClassName(styles, 'uikit-button', { - style, - accessory: context === BLOCK_CONTEXT.SECTION, - action: context === BLOCK_CONTEXT.ACTION, - })} - disabled={performingAction} - type='button' - onClick={handleClick} - onMouseUp={handleMouseUp} - />; + const handleClick = useCallback( + async (event) => { + event.preventDefault(); + + if (confirm) { + // TODO + } + + if (url) { + const newTab = window.open(); + newTab.opener = null; + newTab.location = url; + return; + } + + await performAction({ value }); + }, + [confirm, performAction, url, value], + ); + + return ( + <button + children={parser.text(text)} + className={createClassName(styles, 'uikit-button', { + style, + accessory: context === BLOCK_CONTEXT.SECTION, + action: context === BLOCK_CONTEXT.ACTION, + })} + disabled={performingAction} + type='button' + onClick={handleClick} + onMouseUp={handleMouseUp} + /> + ); }; export default memo(ButtonElement); diff --git a/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js b/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js index 0239f481c60..9655a46db59 100644 --- a/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js +++ b/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js @@ -11,13 +11,15 @@ export default { layout: 'centered', }, decorators: [ - (storyFn) => <Surface - children={storyFn()} - dispatchAction={async (payload) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - action('dispatchAction')(payload); - }} - />, + (storyFn) => ( + <Surface + children={storyFn()} + dispatchAction={async (payload) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + action('dispatchAction')(payload); + }} + /> + ), ], argTypes: { text: { control: 'object' }, @@ -38,6 +40,5 @@ export default { }, }; -export const Default = (args) => - <ButtonElement {...args} parser={parser} />; +export const Default = (args) => <ButtonElement {...args} parser={parser} />; Default.storyName = 'default'; diff --git a/packages/livechat/src/components/uiKit/message/ContextBlock/index.js b/packages/livechat/src/components/uiKit/message/ContextBlock/index.js index 83e203cc99a..d3546d3061c 100644 --- a/packages/livechat/src/components/uiKit/message/ContextBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/ContextBlock/index.js @@ -5,14 +5,16 @@ import { createClassName } from '../../../helpers'; import Block from '../Block'; import styles from './styles.scss'; -const ContextBlock = ({ appId, blockId, elements, parser }) => +const ContextBlock = ({ appId, blockId, elements, parser }) => ( <Block appId={appId} blockId={blockId}> <div className={createClassName(styles, 'uikit-context-block')}> - {elements.map((element, key) => + {elements.map((element, key) => ( <div key={key} className={createClassName(styles, 'uikit-context-block__item')}> {parser.renderContext(element, BLOCK_CONTEXT.CONTEXT)} - </div>)} + </div> + ))} </div> - </Block>; + </Block> +); export default memo(ContextBlock); diff --git a/packages/livechat/src/components/uiKit/message/DatePickerElement/index.js b/packages/livechat/src/components/uiKit/message/DatePickerElement/index.js index 0bb2f4e04ee..c5fa758ff60 100644 --- a/packages/livechat/src/components/uiKit/message/DatePickerElement/index.js +++ b/packages/livechat/src/components/uiKit/message/DatePickerElement/index.js @@ -3,29 +3,34 @@ import { memo, useCallback } from 'preact/compat'; import DateInput from '../../../Form/DateInput'; import { usePerformAction } from '../Block'; -const DatePickerElement = ({ actionId, confirm/* , placeholder */, initialDate/* , parser */ }) => { +const DatePickerElement = ({ actionId, confirm /* , placeholder */, initialDate /* , parser */ }) => { const [performAction, performingAction] = usePerformAction(actionId); - const handleChange = useCallback(async (event) => { - event.preventDefault(); + const handleChange = useCallback( + async (event) => { + event.preventDefault(); - if (confirm) { - // TODO - } + if (confirm) { + // TODO + } - await performAction({ - initialDate, - selectedDate: event.target.value, - }); - }, [confirm, initialDate, performAction]); + await performAction({ + initialDate, + selectedDate: event.target.value, + }); + }, + [confirm, initialDate, performAction], + ); - return <DateInput - value={initialDate} - disabled={performingAction} - // TODO: placeholder={parser.text(placeholder)} - small - onChange={handleChange} - />; + return ( + <DateInput + value={initialDate} + disabled={performingAction} + // TODO: placeholder={parser.text(placeholder)} + small + onChange={handleChange} + /> + ); }; export default memo(DatePickerElement); diff --git a/packages/livechat/src/components/uiKit/message/DividerBlock/index.js b/packages/livechat/src/components/uiKit/message/DividerBlock/index.js index f4d0e98b091..81ac8cb4c1a 100644 --- a/packages/livechat/src/components/uiKit/message/DividerBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/DividerBlock/index.js @@ -4,11 +4,10 @@ import { createClassName } from '../../../helpers'; import Block from '../Block'; import styles from './styles.scss'; -const DividerBlock = ({ appId, blockId }) => +const DividerBlock = ({ appId, blockId }) => ( <Block appId={appId} blockId={blockId}> - <hr - className={createClassName(styles, 'uikit-divider-block')} - /> - </Block>; + <hr className={createClassName(styles, 'uikit-divider-block')} /> + </Block> +); export default memo(DividerBlock); diff --git a/packages/livechat/src/components/uiKit/message/ImageBlock/index.js b/packages/livechat/src/components/uiKit/message/ImageBlock/index.js index e2545e6e56e..15e1dcd90ee 100644 --- a/packages/livechat/src/components/uiKit/message/ImageBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/ImageBlock/index.js @@ -7,11 +7,7 @@ import styles from './styles.scss'; const MAX_SIZE = 360; const ImageBlock = ({ appId, blockId, title, imageUrl, altText, parser }) => { - const [{ - loading, - naturalWidth, - naturalHeight, - }, updateImageState] = useState(() => ({ + const [{ loading, naturalWidth, naturalHeight }, updateImageState] = useState(() => ({ loading: true, naturalWidth: MAX_SIZE, naturalHeight: MAX_SIZE, @@ -40,44 +36,47 @@ const ImageBlock = ({ appId, blockId, title, imageUrl, altText, parser }) => { }; }, [imageUrl]); - const contentStyle = useMemo(() => ({ - maxWidth: Math.min(MAX_SIZE, naturalWidth / naturalHeight * MAX_SIZE), - }), [naturalHeight, naturalWidth]); + const contentStyle = useMemo( + () => ({ + maxWidth: Math.min(MAX_SIZE, (naturalWidth / naturalHeight) * MAX_SIZE), + }), + [naturalHeight, naturalWidth], + ); - const wrapperStyle = useMemo(() => ({ - paddingBottom: `${ naturalHeight / naturalWidth * 100 }%`, - }), [naturalHeight, naturalWidth]); + const wrapperStyle = useMemo( + () => ({ + paddingBottom: `${(naturalHeight / naturalWidth) * 100}%`, + }), + [naturalHeight, naturalWidth], + ); - const linkStyle = useMemo(() => ({ - backgroundImage: `url(${ imageUrl })`, - }), [imageUrl]); + const linkStyle = useMemo( + () => ({ + backgroundImage: `url(${imageUrl})`, + }), + [imageUrl], + ); - return <Block appId={appId} blockId={blockId}> - <div className={createClassName(styles, 'uikit-image-block')}> - {title && <h3 className={createClassName(styles, 'uikit-image-block__title')}> - {parser.text(title)} - </h3>} - <div - className={createClassName(styles, 'uikit-image-block__content', { loading })} - style={contentStyle} - > - <div - className={createClassName(styles, 'uikit-image-block__wrapper')} - style={wrapperStyle} - > - <a - children={imageUrl} - className={createClassName(styles, 'uikit-image-block__link')} - href={imageUrl} - rel='noopener noreferrer' - style={linkStyle} - target='_blank' - title={altText} - /> + return ( + <Block appId={appId} blockId={blockId}> + <div className={createClassName(styles, 'uikit-image-block')}> + {title && <h3 className={createClassName(styles, 'uikit-image-block__title')}>{parser.text(title)}</h3>} + <div className={createClassName(styles, 'uikit-image-block__content', { loading })} style={contentStyle}> + <div className={createClassName(styles, 'uikit-image-block__wrapper')} style={wrapperStyle}> + <a + children={imageUrl} + className={createClassName(styles, 'uikit-image-block__link')} + href={imageUrl} + rel='noopener noreferrer' + style={linkStyle} + target='_blank' + title={altText} + /> + </div> </div> </div> - </div> - </Block>; + </Block> + ); }; export default memo(ImageBlock); diff --git a/packages/livechat/src/components/uiKit/message/ImageElement/index.js b/packages/livechat/src/components/uiKit/message/ImageElement/index.js index 86ef68bb6cd..198b773a621 100644 --- a/packages/livechat/src/components/uiKit/message/ImageElement/index.js +++ b/packages/livechat/src/components/uiKit/message/ImageElement/index.js @@ -4,7 +4,7 @@ import { memo } from 'preact/compat'; import { createClassName } from '../../../helpers'; import styles from './styles.scss'; -const ImageElement = ({ imageUrl, altText, context }) => +const ImageElement = ({ imageUrl, altText, context }) => ( <div aria-label={altText} className={createClassName(styles, 'uikit-image', { @@ -13,9 +13,10 @@ const ImageElement = ({ imageUrl, altText, context }) => })} role='img' style={{ - backgroundImage: `url(${ imageUrl })`, + backgroundImage: `url(${imageUrl})`, }} title={altText} - />; + /> +); export default memo(ImageElement); diff --git a/packages/livechat/src/components/uiKit/message/Mrkdwn/index.js b/packages/livechat/src/components/uiKit/message/Mrkdwn/index.js index 9a3ed03169d..5080b132050 100644 --- a/packages/livechat/src/components/uiKit/message/Mrkdwn/index.js +++ b/packages/livechat/src/components/uiKit/message/Mrkdwn/index.js @@ -5,12 +5,13 @@ import { renderMarkdown } from '../../../Messages/MessageText/markdown'; import { createClassName } from '../../../helpers'; import styles from './styles.scss'; -const Mrkdwn = ({ text/* , verbatim = false */ }) => +const Mrkdwn = ({ text /* , verbatim = false */ }) => ( <div className={createClassName(styles, 'uikit-mrkdwn')} // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: renderEmojis(renderMarkdown(text)) }} dir='auto' - />; + /> +); export default memo(Mrkdwn); diff --git a/packages/livechat/src/components/uiKit/message/OverflowElement/index.js b/packages/livechat/src/components/uiKit/message/OverflowElement/index.js index a381c926141..ab7b170e5ad 100644 --- a/packages/livechat/src/components/uiKit/message/OverflowElement/index.js +++ b/packages/livechat/src/components/uiKit/message/OverflowElement/index.js @@ -12,54 +12,62 @@ const OverflowTrigger = ({ loading, onClick }) => { target.blur(); }, []); - return <Button - className={createClassName(styles, 'uikit-overflow__trigger')} - disabled={loading} - outline - secondary - onClick={onClick} - onMouseUp={handleMouseUp} - > - <KebabIcon width={20} height={20} /> - </Button>; + return ( + <Button + className={createClassName(styles, 'uikit-overflow__trigger')} + disabled={loading} + outline + secondary + onClick={onClick} + onMouseUp={handleMouseUp} + > + <KebabIcon width={20} height={20} /> + </Button> + ); }; const OverflowOption = ({ confirm, text, value, url, parser, onClick }) => { - const handleClick = useCallback(async (event) => { - event.preventDefault(); + const handleClick = useCallback( + async (event) => { + event.preventDefault(); - if (confirm) { - // TODO - } + if (confirm) { + // TODO + } - if (url) { - const newTab = window.open(); - newTab.opener = null; - newTab.location = url; - return; - } + if (url) { + const newTab = window.open(); + newTab.opener = null; + newTab.location = url; + return; + } - await onClick(value); - }, [confirm, onClick, url, value]); + await onClick(value); + }, + [confirm, onClick, url, value], + ); - return <Menu.Item onClick={handleClick}> - {parser.text(text)} - </Menu.Item>; + return <Menu.Item onClick={handleClick}>{parser.text(text)}</Menu.Item>; }; const OverflowElement = ({ actionId, confirm, options, parser }) => { const [performAction, performingAction] = usePerformAction(actionId); - const handleClick = useCallback(async (value) => { - await performAction({ value }); - }, [performAction]); + const handleClick = useCallback( + async (value) => { + await performAction({ value }); + }, + [performAction], + ); - return <PopoverMenu trigger={({ pop }) => <OverflowTrigger loading={performingAction} onClick={pop} />}> - <Menu.Group> - {Array.isArray(options) && options.map((option, i) => - <OverflowOption key={i} {...option} confirm={confirm} parser={parser} onClick={handleClick} />)} - </Menu.Group> - </PopoverMenu>; + return ( + <PopoverMenu trigger={({ pop }) => <OverflowTrigger loading={performingAction} onClick={pop} />}> + <Menu.Group> + {Array.isArray(options) && + options.map((option, i) => <OverflowOption key={i} {...option} confirm={confirm} parser={parser} onClick={handleClick} />)} + </Menu.Group> + </PopoverMenu> + ); }; export default memo(OverflowElement); diff --git a/packages/livechat/src/components/uiKit/message/PlainText/index.js b/packages/livechat/src/components/uiKit/message/PlainText/index.js index e39309bffa4..b56571f9945 100644 --- a/packages/livechat/src/components/uiKit/message/PlainText/index.js +++ b/packages/livechat/src/components/uiKit/message/PlainText/index.js @@ -5,14 +5,9 @@ import { createClassName } from '../../../helpers'; import styles from './styles.scss'; const escapeHtml = (unsafe) => - unsafe - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + unsafe.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); -const PlainText = ({ text, emoji = false }) => +const PlainText = ({ text, emoji = false }) => ( <span className={createClassName(styles, 'uikit-plain-text')} // eslint-disable-next-line react/no-danger @@ -20,6 +15,7 @@ const PlainText = ({ text, emoji = false }) => __html: escapeHtml(emoji ? text : shortnameToUnicode(text)).replace(/\n/g, () => '<br/>'), }} dir='auto' - />; + /> +); export default memo(PlainText); diff --git a/packages/livechat/src/components/uiKit/message/SectionBlock/index.js b/packages/livechat/src/components/uiKit/message/SectionBlock/index.js index 1e237584cf7..01c5f66d4c9 100644 --- a/packages/livechat/src/components/uiKit/message/SectionBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/SectionBlock/index.js @@ -5,15 +5,11 @@ import { createClassName } from '../../../helpers'; import Block from '../Block'; import styles from './styles.scss'; -const SectionBlock = ({ appId, blockId, text, fields, accessory, parser }) => +const SectionBlock = ({ appId, blockId, text, fields, accessory, parser }) => ( <Block appId={appId} blockId={blockId}> <div className={createClassName(styles, 'uikit-section-block')}> <div className={createClassName(styles, 'uikit-section-block__content')}> - {text && ( - <div className={createClassName(styles, 'uikit-section-block__text')}> - {parser.text(text, BLOCK_CONTEXT.SECTION)} - </div> - )} + {text && <div className={createClassName(styles, 'uikit-section-block__text')}>{parser.text(text, BLOCK_CONTEXT.SECTION)}</div>} {Array.isArray(fields) && fields.length > 0 && ( <div className={createClassName(styles, 'uikit-section-block__fields')}> {fields.map((field, i) => ( @@ -24,10 +20,13 @@ const SectionBlock = ({ appId, blockId, text, fields, accessory, parser }) => </div> )} </div> - {accessory && <div className={createClassName(styles, 'uikit-section-block__accessory')}> - {parser.renderAccessories(accessory, BLOCK_CONTEXT.SECTION)} - </div>} + {accessory && ( + <div className={createClassName(styles, 'uikit-section-block__accessory')}> + {parser.renderAccessories(accessory, BLOCK_CONTEXT.SECTION)} + </div> + )} </div> - </Block>; + </Block> +); export default memo(SectionBlock); diff --git a/packages/livechat/src/components/uiKit/message/StaticSelectElement/index.js b/packages/livechat/src/components/uiKit/message/StaticSelectElement/index.js index 865b680f5ac..290c5a20a13 100644 --- a/packages/livechat/src/components/uiKit/message/StaticSelectElement/index.js +++ b/packages/livechat/src/components/uiKit/message/StaticSelectElement/index.js @@ -5,35 +5,44 @@ import { createClassName } from '../../../helpers'; import { usePerformAction } from '../Block'; import styles from './styles.scss'; -const StaticSelectElement = ({ actionId, confirm, placeholder, options/* , optionGroups */, initialOption, parser }) => { +const StaticSelectElement = ({ actionId, confirm, placeholder, options /* , optionGroups */, initialOption, parser }) => { const [performAction, performingAction] = usePerformAction(actionId); - const handleChange = useCallback(async (event) => { - event.preventDefault(); - - if (confirm) { - // TODO - } - - await performAction({ - value: event.target.value, - }); - }, [confirm, performAction]); - - const selectOptions = useMemo(() => options.map((option) => ({ - label: parser.text(option.text), - value: option.value, - })), [options, parser]); - - return <SelectInput - className={createClassName(styles, 'uikit-static-select')} - disabled={performingAction} - options={selectOptions} - placeholder={placeholder && parser.text(placeholder)} - small - value={(initialOption && initialOption.value) || ''} - onChange={handleChange} - />; + const handleChange = useCallback( + async (event) => { + event.preventDefault(); + + if (confirm) { + // TODO + } + + await performAction({ + value: event.target.value, + }); + }, + [confirm, performAction], + ); + + const selectOptions = useMemo( + () => + options.map((option) => ({ + label: parser.text(option.text), + value: option.value, + })), + [options, parser], + ); + + return ( + <SelectInput + className={createClassName(styles, 'uikit-static-select')} + disabled={performingAction} + options={selectOptions} + placeholder={placeholder && parser.text(placeholder)} + small + value={(initialOption && initialOption.value) || ''} + onChange={handleChange} + /> + ); }; export default memo(StaticSelectElement); diff --git a/packages/livechat/src/components/uiKit/message/Surface.js b/packages/livechat/src/components/uiKit/message/Surface.js index 05baed39eb7..fdc8d4955cf 100644 --- a/packages/livechat/src/components/uiKit/message/Surface.js +++ b/packages/livechat/src/components/uiKit/message/Surface.js @@ -5,15 +5,15 @@ const SurfaceContext = createContext({ dispatchAction: () => undefined, }); -const Surface = ({ children, dispatchAction }) => +const Surface = ({ children, dispatchAction }) => ( <SurfaceContext.Provider children={children} value={{ dispatchAction, }} - />; + /> +); -export const useDispatchAction = () => - useContext(SurfaceContext).dispatchAction; +export const useDispatchAction = () => useContext(SurfaceContext).dispatchAction; export default memo(Surface); diff --git a/packages/livechat/src/components/uiKit/message/actions.stories.js b/packages/livechat/src/components/uiKit/message/actions.stories.js index 64cea4dd145..482f807e1cd 100644 --- a/packages/livechat/src/components/uiKit/message/actions.stories.js +++ b/packages/livechat/src/components/uiKit/message/actions.stories.js @@ -10,13 +10,15 @@ export default { }, decorators: [ (storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />, - (storyFn) => <Surface - children={storyFn()} - dispatchAction={async (payload) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - action('dispatchAction')(payload); - }} - />, + (storyFn) => ( + <Surface + children={storyFn()} + dispatchAction={async (payload) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + action('dispatchAction')(payload); + }} + /> + ), ], }; @@ -101,9 +103,7 @@ export const FilteredConversationsSelect = () => emoji: true, }, filter: { - include: [ - 'private', - ], + include: ['private'], }, }, ], diff --git a/packages/livechat/src/components/uiKit/message/context.stories.js b/packages/livechat/src/components/uiKit/message/context.stories.js index 7482740a7dd..2446351ac54 100644 --- a/packages/livechat/src/components/uiKit/message/context.stories.js +++ b/packages/livechat/src/components/uiKit/message/context.stories.js @@ -6,9 +6,7 @@ export default { parameters: { layout: 'centered', }, - decorators: [ - (storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />, - ], + decorators: [(storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />], }; export const PlainText = () => diff --git a/packages/livechat/src/components/uiKit/message/divider.stories.js b/packages/livechat/src/components/uiKit/message/divider.stories.js index e1840a6a2a4..0e01c4be1c2 100644 --- a/packages/livechat/src/components/uiKit/message/divider.stories.js +++ b/packages/livechat/src/components/uiKit/message/divider.stories.js @@ -5,9 +5,7 @@ export default { parameters: { layout: 'centered', }, - decorators: [ - (storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />, - ], + decorators: [(storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />], }; export const Default = () => diff --git a/packages/livechat/src/components/uiKit/message/image.stories.js b/packages/livechat/src/components/uiKit/message/image.stories.js index d25395c24f2..50681475bbb 100644 --- a/packages/livechat/src/components/uiKit/message/image.stories.js +++ b/packages/livechat/src/components/uiKit/message/image.stories.js @@ -6,9 +6,7 @@ export default { parameters: { layout: 'centered', }, - decorators: [ - (storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />, - ], + decorators: [(storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />], }; export const WithTitle = () => diff --git a/packages/livechat/src/components/uiKit/message/index.js b/packages/livechat/src/components/uiKit/message/index.js index f3106bc9472..1424e89d581 100644 --- a/packages/livechat/src/components/uiKit/message/index.js +++ b/packages/livechat/src/components/uiKit/message/index.js @@ -102,8 +102,7 @@ class MessageParser extends UiKitParserMessage { return <StaticSelectElement key={index} {...element} parser={this} context={context} />; }; - multiStaticSelect = () => - null; + multiStaticSelect = () => null; } export const parser = new MessageParser(); diff --git a/packages/livechat/src/components/uiKit/message/section.stories.js b/packages/livechat/src/components/uiKit/message/section.stories.js index c71c55ecd3d..5b602628600 100644 --- a/packages/livechat/src/components/uiKit/message/section.stories.js +++ b/packages/livechat/src/components/uiKit/message/section.stories.js @@ -13,13 +13,15 @@ export default { decorators: [ (storyFn) => <div children={storyFn()} style={{ width: '100vw', maxWidth: 500 }} />, (storyFn) => <PopoverContainer children={storyFn()} />, - (storyFn) => <Surface - children={storyFn()} - dispatchAction={async (payload) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - action('dispatchAction')(payload); - }} - />, + (storyFn) => ( + <Surface + children={storyFn()} + dispatchAction={async (payload) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + action('dispatchAction')(payload); + }} + /> + ), ], }; diff --git a/packages/livechat/src/helpers.stories.js b/packages/livechat/src/helpers.stories.js index 281adf955a9..54a6c141e2e 100644 --- a/packages/livechat/src/helpers.stories.js +++ b/packages/livechat/src/helpers.stories.js @@ -8,17 +8,12 @@ import tassoAvatar from '../.storybook/assets/tasso.jpg'; export const centered = (storyFn) => ( <div style={{ position: 'fixed', top: 0, left: 0, bottom: 0, right: 0, display: 'flex', alignItems: 'center', overflow: 'auto' }}> - <div style={{ margin: 'auto', maxHeight: '100%' }}> - {storyFn()} - </div> + <div style={{ margin: 'auto', maxHeight: '100%' }}>{storyFn()}</div> </div> ); -export const screenCentered = (storyFn, ...args) => centered(() => ( - <div style={{ display: 'flex', width: '365px', height: '500px' }}> - {storyFn()} - </div> -), ...args); +export const screenCentered = (storyFn, ...args) => + centered(() => <div style={{ display: 'flex', width: '365px', height: '500px' }}>{storyFn()}</div>, ...args); export const screenProps = () => ({ theme: { @@ -36,11 +31,12 @@ export const screenProps = () => ({ onOpenWindow: action('openWindow'), }); -export const avatarResolver = (username) => ({ - 'guilherme.gazzo': gazzoAvatar, - 'martin.schoeler': martinAvatar, - 'tasso.evangelista': tassoAvatar, -})[username]; +export const avatarResolver = (username) => + ({ + 'guilherme.gazzo': gazzoAvatar, + 'martin.schoeler': martinAvatar, + 'tasso.evangelista': tassoAvatar, + }[username]); export const attachmentResolver = (url) => url; @@ -51,8 +47,4 @@ const createRandom = (s) => () => { const loremIpsumRandom = createRandom(42); export const loremIpsum = (options) => originalLoremIpsum({ random: loremIpsumRandom, ...options }); -export { - gazzoAvatar, - martinAvatar, - tassoAvatar, -}; +export { gazzoAvatar, martinAvatar, tassoAvatar }; diff --git a/packages/livechat/src/icons/stories.js b/packages/livechat/src/icons/stories.js index 4d02821fb51..970ecb23fd4 100644 --- a/packages/livechat/src/icons/stories.js +++ b/packages/livechat/src/icons/stories.js @@ -5,13 +5,11 @@ import { storiesOf } from '@storybook/react'; import { centered } from '../helpers.stories'; - const req = require.context('./', true, /\.svg$/); -const iconset = req.keys() - .map((filename) => ({ - component: req(filename), - name: path.basename(filename, '.svg'), - })); +const iconset = req.keys().map((filename) => ({ + component: req(filename), + name: path.basename(filename, '.svg'), +})); const IconDisplay = ({ component: Icon, name, color }) => ( <div @@ -37,7 +35,9 @@ storiesOf('Components/Icons', module) .addDecorator(withKnobs) .add('all', () => ( <div style={{ width: '100%', display: 'flex', flexWrap: 'wrap' }}> - {iconset.map((props) => <IconDisplay color={color('color', '#E0364D')} {...props} />)} + {iconset.map((props) => ( + <IconDisplay color={color('color', '#E0364D')} {...props} /> + ))} </div> )); iconset.forEach(({ component: Icon, name }) => diff --git a/packages/livechat/src/lib/api.js b/packages/livechat/src/lib/api.js index c652d8d392c..277032f32ca 100644 --- a/packages/livechat/src/lib/api.js +++ b/packages/livechat/src/lib/api.js @@ -12,11 +12,12 @@ export const normalizeQueueAlert = (queueInfo) => { const { spot, estimatedWaitTimeSeconds } = queueInfo; const locale = getDateFnsLocale(); - const estimatedWaitTime = estimatedWaitTimeSeconds && formatDistance(new Date().setSeconds(estimatedWaitTimeSeconds), new Date(), { locale }); - return spot > 0 - && ( - estimatedWaitTime + const estimatedWaitTime = + estimatedWaitTimeSeconds && formatDistance(new Date().setSeconds(estimatedWaitTimeSeconds), new Date(), { locale }); + return ( + spot > 0 && + (estimatedWaitTime ? i18next.t('your_spot_is_spot_estimated_wait_time_estimatedwai', { spot, estimatedWaitTime }) - : i18next.t('your_spot_is_spot', { spot }) + : i18next.t('your_spot_is_spot', { spot })) ); }; diff --git a/packages/livechat/src/lib/email.js b/packages/livechat/src/lib/email.js index 1bab788e585..cab81f38084 100644 --- a/packages/livechat/src/lib/email.js +++ b/packages/livechat/src/lib/email.js @@ -1,5 +1,6 @@ const basicEmailRegex = /^[^@]+@[^@]+$/; -const rfcEmailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +const rfcEmailRegex = + /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; export const validateEmail = (email, options = { style: 'basic' }) => { switch (options.style) { diff --git a/packages/livechat/src/lib/hooks.js b/packages/livechat/src/lib/hooks.js index 584a29f27fc..46c2c11fa0f 100644 --- a/packages/livechat/src/lib/hooks.js +++ b/packages/livechat/src/lib/hooks.js @@ -11,13 +11,18 @@ import Triggers from './triggers'; const createOrUpdateGuest = async (guest) => { const { token } = guest; - token && await store.setState({ token }); + token && (await store.setState({ token })); const user = await Livechat.grantVisitor({ visitor: { ...guest } }); store.setState({ user }); }; const updateIframeGuestData = (data) => { - const { iframe, iframe: { guest }, user: _id, token } = store.state; + const { + iframe, + iframe: { guest }, + user: _id, + token, + } = store.state; store.setState({ iframe: { ...iframe, guest: { ...guest, ...data } } }); if (!_id) { @@ -35,7 +40,11 @@ const api = { } const { token, room: { _id: rid } = {} } = store.state; - const { change, title, location: { href } } = info; + const { + change, + title, + location: { href }, + } = info; Livechat.sendVisitorNavigation({ token, rid, pageInfo: { change, title, location: { href } } }); }, @@ -45,7 +54,10 @@ const api = { }, setTheme({ color, fontColor, iconColor, title, offlineTitle } = {}) { - const { iframe, iframe: { theme } } = store.state; + const { + iframe, + iframe: { theme }, + } = store.state; store.setState({ iframe: { ...iframe, @@ -62,7 +74,10 @@ const api = { }, async setDepartment(value) { - const { config: { departments = [] }, user: { department: existingDepartment } = {} } = store.state; + const { + config: { departments = [] }, + user: { department: existingDepartment } = {}, + } = store.state; const department = departments.find((dep) => dep._id === value || dep.name === value)?._id || ''; @@ -113,7 +128,11 @@ const api = { }, async setGuestToken(token) { - const { token: localToken, iframe, iframe: { guest } } = store.state; + const { + token: localToken, + iframe, + iframe: { guest }, + } = store.state; if (token === localToken) { return; } diff --git a/packages/livechat/src/lib/locale.js b/packages/livechat/src/lib/locale.js index c0b3c876688..d5438b598c5 100644 --- a/packages/livechat/src/lib/locale.js +++ b/packages/livechat/src/lib/locale.js @@ -17,7 +17,7 @@ export const normalizeLanguageString = (languageString) => { countryCode = countryCode.toUpperCase(); } - return countryCode ? `${ languageCode }-${ countryCode }` : languageCode; + return countryCode ? `${languageCode}-${countryCode}` : languageCode; }; /** @@ -35,13 +35,66 @@ export const configLanguage = () => { export const getDateFnsLocale = () => { const supportedLocales = [ - 'af', 'ar', 'be', 'bg', 'bn', 'ca', 'cs', 'cy', 'da', 'de', - 'el', 'en-AU', 'en-CA', 'en-GB', 'en-US', 'eo', 'es', 'et', - 'fa-IR', 'fi', 'fr', 'fr-CA', 'gl', 'gu', 'he', 'hi', 'hr', - 'hu', 'hy', 'id', 'is', 'it', 'ja', 'ka', 'kk', 'ko', 'lt', - 'lv', 'nb', 'nl', 'nn', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'sk', - 'sl', 'sr', 'sr-Latn', 'sv', 'ta', 'te', 'th', 'tr', 'ug', - 'uk', 'vi', 'zh_CN', 'zh_TW', + 'af', + 'ar', + 'be', + 'bg', + 'bn', + 'ca', + 'cs', + 'cy', + 'da', + 'de', + 'el', + 'en-AU', + 'en-CA', + 'en-GB', + 'en-US', + 'eo', + 'es', + 'et', + 'fa-IR', + 'fi', + 'fr', + 'fr-CA', + 'gl', + 'gu', + 'he', + 'hi', + 'hr', + 'hu', + 'hy', + 'id', + 'is', + 'it', + 'ja', + 'ka', + 'kk', + 'ko', + 'lt', + 'lv', + 'nb', + 'nl', + 'nn', + 'pl', + 'pt', + 'pt-BR', + 'ro', + 'ru', + 'sk', + 'sl', + 'sr', + 'sr-Latn', + 'sv', + 'ta', + 'te', + 'th', + 'tr', + 'ug', + 'uk', + 'vi', + 'zh_CN', + 'zh_TW', ]; let fullLanguage = configLanguage() || browserLanguage(); @@ -49,5 +102,5 @@ export const getDateFnsLocale = () => { const [languageCode] = fullLanguage.split ? fullLanguage.split(/[-_]/) : []; const locale = [fullLanguage, languageCode, 'en-US'].find((lng) => supportedLocales.indexOf(lng) > -1); // eslint-disable-next-line import/no-dynamic-require - return require(`date-fns/locale/${ locale }/index.js`); + return require(`date-fns/locale/${locale}/index.js`); }; diff --git a/packages/livechat/src/lib/main.js b/packages/livechat/src/lib/main.js index c099743a523..9caad8efefd 100644 --- a/packages/livechat/src/lib/main.js +++ b/packages/livechat/src/lib/main.js @@ -6,17 +6,14 @@ import store from '../store'; import constants from './constants'; export const updateBusinessUnit = async (newBusinessUnit) => { - const { - token, - config: existingConfig, - } = store.state; + const { token, config: existingConfig } = store.state; if (!token) { throw new Error('Error! no livechat token found. please make sure you initialize widget first before setting business unit'); } const { departments } = await Livechat.config({ token, - ...newBusinessUnit && { businessUnit: newBusinessUnit }, + ...(newBusinessUnit && { businessUnit: newBusinessUnit }), }); if (newBusinessUnit) { @@ -39,11 +36,7 @@ export const updateBusinessUnit = async (newBusinessUnit) => { }; export const loadConfig = async () => { - const { - token, - businessUnit = null, - iframe: { guest: { department } = {} } = {}, - } = store.state; + const { token, businessUnit = null, iframe: { guest: { department } = {} } = {} } = store.state; Livechat.credentials.token = token; @@ -56,8 +49,8 @@ export const loadConfig = async () => { ...config } = await Livechat.config({ token, - ...businessUnit && { businessUnit }, - ...department && { department }, + ...(businessUnit && { businessUnit }), + ...(department && { department }), }); await store.setState({ diff --git a/packages/livechat/src/lib/parentCall.js b/packages/livechat/src/lib/parentCall.js index c0fa66d0eb2..8ed6a79d33e 100644 --- a/packages/livechat/src/lib/parentCall.js +++ b/packages/livechat/src/lib/parentCall.js @@ -11,4 +11,5 @@ export function parentCall(method, args = []) { window.parent.postMessage(data, '*'); // lgtm [js/cross-window-information-leak] } -export const runCallbackEventEmitter = (callbackName, data) => validCallbacks.includes(callbackName) && parentCall('callback', [callbackName, data]); +export const runCallbackEventEmitter = (callbackName, data) => + validCallbacks.includes(callbackName) && parentCall('callback', [callbackName, data]); diff --git a/packages/livechat/src/lib/random.js b/packages/livechat/src/lib/random.js index d5860c4e949..ca62153c4ae 100644 --- a/packages/livechat/src/lib/random.js +++ b/packages/livechat/src/lib/random.js @@ -1,4 +1,3 @@ - import * as crypto from 'crypto-js'; const UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'; @@ -19,10 +18,8 @@ export const chooseElement = (arrayOrString) => { return arrayOrString[index]; }; -export const createRandomString = (charsCount, alphabet) => - Array.from({ length: charsCount }, () => chooseElement(alphabet)).join(''); +export const createRandomString = (charsCount, alphabet) => Array.from({ length: charsCount }, () => chooseElement(alphabet)).join(''); -export const createRandomId = (charsCount = 17) => - createRandomString(charsCount, UNMISTAKABLE_CHARS); +export const createRandomId = (charsCount = 17) => createRandomString(charsCount, UNMISTAKABLE_CHARS); export const createToken = () => crypto.lib.WordArray.random(32).toString(crypto.enc.Hex); diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index a15256ca4f4..1c7afea23d7 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -211,7 +211,12 @@ Livechat.onMessage(async (message) => { message = transformAgentInformationOnMessage(message); await store.setState({ - messages: upsert(store.state.messages, message, ({ _id }) => _id === message._id, ({ ts }) => ts), + messages: upsert( + store.state.messages, + message, + ({ _id }) => _id === message._id, + ({ ts }) => ts, + ), }); await processMessage(message); diff --git a/packages/livechat/src/lib/threads.js b/packages/livechat/src/lib/threads.js index 2af951adf55..6377627e0e5 100644 --- a/packages/livechat/src/lib/threads.js +++ b/packages/livechat/src/lib/threads.js @@ -9,7 +9,14 @@ const addParentMessage = async (parentMessage) => { const { tmid } = parentMessage; if (!parentMessages.find((msg) => msg._id === tmid)) { - await store.setState({ parentMessages: upsert(parentMessages, parentMessage, ({ _id }) => _id === parentMessage._id, ({ ts }) => ts) }); + await store.setState({ + parentMessages: upsert( + parentMessages, + parentMessage, + ({ _id }) => _id === parentMessage._id, + ({ ts }) => ts, + ), + }); } }; @@ -33,7 +40,9 @@ const findParentMessage = async (tmid) => { parentMessage = await Livechat.message(tmid, { rid }); await addParentMessage(parentMessage); } catch (error) { - const { data: { error: reason } } = error; + const { + data: { error: reason }, + } = error; const alert = { id: createToken(), children: reason, error: true, timeout: 5000 }; await store.setState({ alerts: (alerts.push(alert), alerts) }); } @@ -71,10 +80,8 @@ export const normalizeMessage = async (message) => { export const normalizeMessages = (messages = []) => Promise.all( - messages.filter( - async (message) => { - const result = await normalizeMessage(message); - return result; - }, - ), + messages.filter(async (message) => { + const result = await normalizeMessage(message); + return result; + }), ); diff --git a/packages/livechat/src/lib/transcript.js b/packages/livechat/src/lib/transcript.js index 834e403f240..970aab2ee9b 100644 --- a/packages/livechat/src/lib/transcript.js +++ b/packages/livechat/src/lib/transcript.js @@ -4,10 +4,14 @@ import { Livechat } from '../api'; import { ModalManager } from '../components/Modal'; import store from '../store'; - const promptTranscript = async () => { - console.log(store.state); - const { config: { messages: { transcriptMessage } }, user: { token, visitorEmails }, room: { _id } } = store.state; + const { + config: { + messages: { transcriptMessage }, + }, + user: { token, visitorEmails }, + room: { _id }, + } = store.state; const email = visitorEmails && visitorEmails.length > 0 ? visitorEmails[0].address : ''; if (!email) { return; @@ -18,20 +22,22 @@ const promptTranscript = async () => { return ModalManager.confirm({ text: message, }).then((result) => { - if ((typeof result.success === 'boolean') && result.success) { + if (typeof result.success === 'boolean' && result.success) { return Livechat.requestTranscript(email, { token, rid: _id }); } }); }; -const transcriptSentAlert = (message) => ModalManager.alert({ - text: message, - timeout: 1000, -}); - +const transcriptSentAlert = (message) => + ModalManager.alert({ + text: message, + timeout: 1000, + }); export const handleTranscript = async () => { - const { config: { settings: { transcript } = {} } } = store.state; + const { + config: { settings: { transcript } = {} }, + } = store.state; if (!transcript) { return; diff --git a/packages/livechat/src/lib/triggers.js b/packages/livechat/src/lib/triggers.js index 98c1bf9e65a..f366ae1e46b 100644 --- a/packages/livechat/src/lib/triggers.js +++ b/packages/livechat/src/lib/triggers.js @@ -21,7 +21,12 @@ const getAgent = (triggerAction) => { if (params.sender === 'queue') { const { state } = store; - const { defaultAgent, iframe: { guest: { department } } } = state; + const { + defaultAgent, + iframe: { + guest: { department }, + }, + } = state; if (defaultAgent && defaultAgent.ts && Date.now() - defaultAgent.ts < agentCacheExpiry) { return resolve(defaultAgent); // cache valid for 1 } @@ -71,7 +76,11 @@ class Triggers { return; } - const { token, firedTriggers = [], config: { triggers } } = store.state; + const { + token, + firedTriggers = [], + config: { triggers }, + } = store.state; Livechat.credentials.token = token; if (!(triggers && triggers.length > 0)) { @@ -114,7 +123,12 @@ class Triggers { await store.setState({ triggered: true, - messages: upsert(store.state.messages, message, ({ _id }) => _id === message._id, ({ ts }) => ts), + messages: upsert( + store.state.messages, + message, + ({ _id }) => _id === message._id, + ({ ts }) => ts, + ), }); await processUnread(); diff --git a/packages/livechat/src/lib/uiKit.js b/packages/livechat/src/lib/uiKit.js index 9b96555d342..efb3da68c01 100644 --- a/packages/livechat/src/lib/uiKit.js +++ b/packages/livechat/src/lib/uiKit.js @@ -99,16 +99,7 @@ const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...data }) return UIKitInteractionType.MODAL_ClOSE; }; -export const triggerAction = async ({ - appId, - type, - actionId, - rid, - mid, - viewId, - container, - payload, -}) => { +export const triggerAction = async ({ appId, type, actionId, rid, mid, viewId, container, payload }) => { const triggerId = generateTriggerId(appId); try { @@ -124,7 +115,7 @@ export const triggerAction = async ({ }; const result = await Promise.race([ - fetch(`${ Livechat.client.host }/api/${ encodeURI(`apps/ui.interaction/${ appId }`) }`, { + 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()), diff --git a/packages/livechat/src/lib/userPresence.js b/packages/livechat/src/lib/userPresence.js index 5be8c22b314..62fc717cb5e 100644 --- a/packages/livechat/src/lib/userPresence.js +++ b/packages/livechat/src/lib/userPresence.js @@ -9,7 +9,6 @@ let self; let oldStatus; const userPrensence = { - init() { if (initiated) { return; diff --git a/packages/livechat/src/routes/Chat/component.js b/packages/livechat/src/routes/Chat/component.js index 79bbd7c7e84..64ee65dc011 100644 --- a/packages/livechat/src/routes/Chat/component.js +++ b/packages/livechat/src/routes/Chat/component.js @@ -100,139 +100,150 @@ class Chat extends Component { } }; - render = ({ - color, - title, - fontColor, - uid, - agent, - typingUsernames, - avatarResolver, - conversationFinishedMessage, - loading, - onUpload, - messages, - uploads = false, - options, - onChangeDepartment, - onFinishChat, - onRemoveUserData, - lastReadMessageId, - queueInfo, - registrationRequired, - onRegisterUser, - limitTextLength, - t, - incomingCallAlert, - ongoingCall, - dispatch, - ...props - }, { - atBottom = true, - text, - }) => <Screen - color={color} - title={title || t('need_help')} - fontColor={fontColor} - agent={agent || null} - queueInfo={queueInfo} - nopadding - onChangeDepartment={onChangeDepartment} - onFinishChat={onFinishChat} - onRemoveUserData={onRemoveUserData} - className={createClassName(styles, 'chat')} - handleEmojiClick={this.handleEmojiClick} - {...props} - > - <FilesDropTarget - ref={this.handleFilesDropTargetRef} - overlayed - overlayText={t('drop_here_to_upload_a_file')} - onUpload={onUpload} + render = ( + { + color, + title, + fontColor, + uid, + agent, + typingUsernames, + avatarResolver, + conversationFinishedMessage, + loading, + onUpload, + messages, + uploads = false, + options, + onChangeDepartment, + onFinishChat, + onRemoveUserData, + lastReadMessageId, + queueInfo, + registrationRequired, + onRegisterUser, + limitTextLength, + t, + incomingCallAlert, + ongoingCall, + dispatch, + ...props + }, + { atBottom = true, text }, + ) => ( + <Screen + color={color} + title={title || t('need_help')} + fontColor={fontColor} + agent={agent || null} + queueInfo={queueInfo} + nopadding + onChangeDepartment={onChangeDepartment} + onFinishChat={onFinishChat} + onRemoveUserData={onRemoveUserData} + className={createClassName(styles, 'chat')} + handleEmojiClick={this.handleEmojiClick} + {...props} > - <Screen.Content nopadding> - { incomingCallAlert && !!incomingCallAlert.show && <CallNotification { ...incomingCallAlert } dispatch={dispatch} />} - { incomingCallAlert?.show && ongoingCall && ongoingCall.callStatus === CallStatus.IN_PROGRESS_SAME_TAB ? <CallIframe { ...incomingCallAlert } /> : null } - <div className={createClassName(styles, 'chat__messages', { atBottom, loading })}> - <MessageList - ref={this.handleMessagesContainerRef} - avatarResolver={avatarResolver} - uid={uid} - messages={messages} - typingUsernames={typingUsernames} - conversationFinishedMessage={conversationFinishedMessage} - lastReadMessageId={lastReadMessageId} - onScrollTo={this.handleScrollTo} - handleEmojiClick={this.handleEmojiClick} - /> - {this.state.emojiPickerActive && <Picker - style={{ position: 'absolute', zIndex: 10, bottom: 0, maxWidth: '90%', left: 20, maxHeight: '90%' }} - showPreview={false} - showSkinTones={false} - sheetSize={64} - onSelect={this.handleEmojiSelect} - autoFocus={true} - />} - </div> - </Screen.Content> - <Screen.Footer - options={options ? ( - <FooterOptions> - <Menu.Group> - {onChangeDepartment && ( - <Menu.Item onClick={onChangeDepartment} icon={ChangeIcon}>{t('change_department')}</Menu.Item> - )} - {onRemoveUserData && ( - <Menu.Item onClick={onRemoveUserData} icon={RemoveIcon}>{t('forget_remove_my_data')}</Menu.Item> - )} - {onFinishChat && ( - <Menu.Item danger onClick={onFinishChat} icon={FinishIcon}>{t('finish_this_chat')}</Menu.Item> - )} - </Menu.Group> - </FooterOptions> - ) : null} - limit={limitTextLength - ? <CharCounter - limitTextLength={limitTextLength} - textLength={text.length} - /> : null} - > - { registrationRequired - ? <Button loading={loading} disabled={loading} onClick={onRegisterUser} stack>{t('chat_now')}</Button> - : <Composer onUpload={onUpload} - onSubmit={this.handleSubmit} - onChange={this.handleChangeText} - placeholder={t('type_your_message_here')} - value={text} - notifyEmojiSelect={(click) => { this.notifyEmojiSelect = click; }} - handleEmojiClick={this.handleEmojiClick} - pre={( - <ComposerActions> - <ComposerAction className={createClassName(styles, 'emoji-picker-icon')} onClick={this.toggleEmojiPickerState}> - <EmojiIcon width={20} height={20} /> - </ComposerAction> - </ComposerActions> + <FilesDropTarget ref={this.handleFilesDropTargetRef} overlayed overlayText={t('drop_here_to_upload_a_file')} onUpload={onUpload}> + <Screen.Content nopadding> + {incomingCallAlert && !!incomingCallAlert.show && <CallNotification {...incomingCallAlert} dispatch={dispatch} />} + {incomingCallAlert?.show && ongoingCall && ongoingCall.callStatus === CallStatus.IN_PROGRESS_SAME_TAB ? ( + <CallIframe {...incomingCallAlert} /> + ) : null} + <div className={createClassName(styles, 'chat__messages', { atBottom, loading })}> + <MessageList + ref={this.handleMessagesContainerRef} + avatarResolver={avatarResolver} + uid={uid} + messages={messages} + typingUsernames={typingUsernames} + conversationFinishedMessage={conversationFinishedMessage} + lastReadMessageId={lastReadMessageId} + onScrollTo={this.handleScrollTo} + handleEmojiClick={this.handleEmojiClick} + /> + {this.state.emojiPickerActive && ( + <Picker + style={{ position: 'absolute', zIndex: 10, bottom: 0, maxWidth: '90%', left: 20, maxHeight: '90%' }} + showPreview={false} + showSkinTones={false} + sheetSize={64} + onSelect={this.handleEmojiSelect} + autoFocus={true} + /> )} - post={( - <ComposerActions> - {text.length === 0 && uploads && ( - <ComposerAction onClick={this.handleUploadClick}> - <PlusIcon width={20} height={20} /> + </div> + </Screen.Content> + <Screen.Footer + options={ + options ? ( + <FooterOptions> + <Menu.Group> + {onChangeDepartment && ( + <Menu.Item onClick={onChangeDepartment} icon={ChangeIcon}> + {t('change_department')} + </Menu.Item> + )} + {onRemoveUserData && ( + <Menu.Item onClick={onRemoveUserData} icon={RemoveIcon}> + {t('forget_remove_my_data')} + </Menu.Item> + )} + {onFinishChat && ( + <Menu.Item danger onClick={onFinishChat} icon={FinishIcon}> + {t('finish_this_chat')} + </Menu.Item> + )} + </Menu.Group> + </FooterOptions> + ) : null + } + limit={limitTextLength ? <CharCounter limitTextLength={limitTextLength} textLength={text.length} /> : null} + > + {registrationRequired ? ( + <Button loading={loading} disabled={loading} onClick={onRegisterUser} stack> + {t('chat_now')} + </Button> + ) : ( + <Composer + onUpload={onUpload} + onSubmit={this.handleSubmit} + onChange={this.handleChangeText} + placeholder={t('type_your_message_here')} + value={text} + notifyEmojiSelect={(click) => { + this.notifyEmojiSelect = click; + }} + handleEmojiClick={this.handleEmojiClick} + pre={ + <ComposerActions> + <ComposerAction className={createClassName(styles, 'emoji-picker-icon')} onClick={this.toggleEmojiPickerState}> + <EmojiIcon width={20} height={20} /> </ComposerAction> - )} - {text.length > 0 && ( - <ComposerAction onClick={this.handleSendClick}> - <SendIcon width={20} height={20} /> - </ComposerAction> - )} - </ComposerActions> - )} - limitTextLength={limitTextLength} - /> - } - </Screen.Footer> - </FilesDropTarget> - </Screen>; + </ComposerActions> + } + post={ + <ComposerActions> + {text.length === 0 && uploads && ( + <ComposerAction onClick={this.handleUploadClick}> + <PlusIcon width={20} height={20} /> + </ComposerAction> + )} + {text.length > 0 && ( + <ComposerAction onClick={this.handleSendClick}> + <SendIcon width={20} height={20} /> + </ComposerAction> + )} + </ComposerActions> + } + limitTextLength={limitTextLength} + /> + )} + </Screen.Footer> + </FilesDropTarget> + </Screen> + ); } export default withTranslation()(Chat); diff --git a/packages/livechat/src/routes/Chat/connector.js b/packages/livechat/src/routes/Chat/connector.js index dc7bab9572e..742e5814612 100644 --- a/packages/livechat/src/routes/Chat/connector.js +++ b/packages/livechat/src/routes/Chat/connector.js @@ -18,22 +18,12 @@ const ChatConnector = ({ ref, ...props }) => ( emailFieldRegistrationForm, limitTextLength, } = {}, - messages: { - conversationFinishedMessage, - } = {}, - theme: { - color, - title, - } = {}, + messages: { conversationFinishedMessage } = {}, + theme: { color, title } = {}, departments = {}, }, iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - title: customTitle, - } = {}, + theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor, title: customTitle } = {}, guest, } = {}, token, @@ -66,18 +56,24 @@ const ChatConnector = ({ ref, ...props }) => ( sound={sound} token={token} user={user} - agent={agent ? { - _id: agent._id, - name: agent.name, - status: agent.status, - email: agent.emails && agent.emails[0] && agent.emails[0].address, - username: agent.username, - phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), - avatar: agent.username ? { - description: agent.username, - src: getAvatarUrl(agent.username), - } : undefined, - } : undefined} + agent={ + agent + ? { + _id: agent._id, + name: agent.name, + status: agent.status, + email: agent.emails && agent.emails[0] && agent.emails[0].address, + username: agent.username, + phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), + avatar: agent.username + ? { + description: agent.username, + src: getAvatarUrl(agent.username), + } + : undefined, + } + : undefined + } room={room} messages={messages && messages.filter((message) => canRenderMessage(message))} noMoreMessages={noMoreMessages} @@ -98,11 +94,15 @@ const ChatConnector = ({ ref, ...props }) => ( lastReadMessageId={lastReadMessageId} guest={guest} triggerAgent={triggerAgent} - queueInfo={queueInfo ? { - spot: queueInfo.spot, - estimatedWaitTimeSeconds: queueInfo.estimatedWaitTimeSeconds, - message: queueInfo.message, - } : undefined} + queueInfo={ + queueInfo + ? { + spot: queueInfo.spot, + estimatedWaitTimeSeconds: queueInfo.estimatedWaitTimeSeconds, + message: queueInfo.message, + } + : undefined + } registrationFormEnabled={registrationForm} nameFieldRegistrationForm={nameFieldRegistrationForm} emailFieldRegistrationForm={emailFieldRegistrationForm} diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js index c13c95d9d0f..df5eb70d319 100644 --- a/packages/livechat/src/routes/Chat/container.js +++ b/packages/livechat/src/routes/Chat/container.js @@ -79,8 +79,15 @@ class ChatContainer extends Component { parentCall('callback', 'chat-started'); return newRoom; } catch (error) { - const { data: { error: reason } } = error; - const alert = { id: createToken(), children: i18n.t('error_starting_a_new_conversation_reason', { reason }), error: true, timeout: 10000 }; + const { + data: { error: reason }, + } = error; + const alert = { + id: createToken(), + children: i18n.t('error_starting_a_new_conversation_reason', { reason }), + error: true, + timeout: 10000, + }; await dispatch({ loading: false, alerts: (alerts.push(alert), alerts) }); runCallbackEventEmitter(reason); @@ -123,10 +130,7 @@ class ChatContainer extends Component { try { this.stopTypingDebounced.stop(); - await Promise.all([ - this.stopTyping({ rid, username: user.username }), - Livechat.sendMessage({ msg, token, rid }), - ]); + await Promise.all([this.stopTyping({ rid, username: user.username }), Livechat.sendMessage({ msg, token, rid })]); } catch (error) { const reason = error?.data?.error ?? error.message; const alert = { id: createToken(), children: reason, error: true, timeout: 5000 }; @@ -141,7 +145,9 @@ class ChatContainer extends Component { try { await Livechat.uploadFile({ rid, file }); } catch (error) { - const { data: { reason, sizeAllowed } } = error; + const { + data: { reason, sizeAllowed }, + } = error; let message = i18n.t('fileupload_error'); switch (reason) { @@ -234,7 +240,7 @@ class ChatContainer extends Component { canFinishChat = () => { const { room, connecting } = this.props; - return (room !== undefined) || connecting; + return room !== undefined || connecting; }; canRemoveUserData = () => { @@ -243,13 +249,7 @@ class ChatContainer extends Component { }; registrationRequired = () => { - const { - registrationFormEnabled, - nameFieldRegistrationForm, - emailFieldRegistrationForm, - departments = [], - user, - } = this.props; + const { registrationFormEnabled, nameFieldRegistrationForm, emailFieldRegistrationForm, departments = [], user } = this.props; if (user && user.token) { return false; @@ -265,9 +265,7 @@ class ChatContainer extends Component { onRegisterUser = () => route('/register'); - showOptionsMenu = () => - this.canSwitchDepartment() || this.canFinishChat() || this.canRemoveUserData(); - + showOptionsMenu = () => this.canSwitchDepartment() || this.canFinishChat() || this.canRemoveUserData(); async handleConnectingAgentAlert(connecting, message) { const { alerts: oldAlerts, dispatch, i18n } = this.props; @@ -306,7 +304,12 @@ class ChatContainer extends Component { const ts = new Date(); const message = { _id: livechatQueueMessageId, msg, u, ts: ts.toISOString() }; await dispatch({ - messages: upsert(messages, message, ({ _id }) => _id === message._id, ({ ts }) => ts), + messages: upsert( + messages, + message, + ({ _id }) => _id === message._id, + ({ ts }) => ts, + ), }); } @@ -322,7 +325,10 @@ class ChatContainer extends Component { if (messages && prevMessages && messages.length !== prevMessages.length && visible && !minimized) { const nextLastMessage = messages[messages.length - 1]; const lastMessage = prevMessages[prevMessages.length - 1]; - if ((nextLastMessage && lastMessage && nextLastMessage._id !== lastMessage._id) || (messages.length === 1 && prevMessages.length === 0)) { + if ( + (nextLastMessage && lastMessage && nextLastMessage._id !== lastMessage._id) || + (messages.length === 1 && prevMessages.length === 0) + ) { const newAlerts = prevAlerts.filter((item) => item.id !== constants.unreadMessagesAlertId); dispatch({ alerts: newAlerts, unread: null, lastReadMessageId: nextLastMessage._id }); } @@ -370,22 +376,12 @@ export const ChatConnector = ({ ref, t, ...props }) => ( emailFieldRegistrationForm, limitTextLength, } = {}, - messages: { - conversationFinishedMessage, - } = {}, - theme: { - color, - title, - } = {}, + messages: { conversationFinishedMessage } = {}, + theme: { color, title } = {}, departments = {}, }, iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - title: customTitle, - } = {}, + theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor, title: customTitle } = {}, guest, } = {}, token, @@ -420,18 +416,24 @@ export const ChatConnector = ({ ref, t, ...props }) => ( sound={sound} token={token} user={user} - agent={agent ? { - _id: agent._id, - name: agent.name, - status: agent.status, - email: agent.emails && agent.emails[0] && agent.emails[0].address, - username: agent.username, - phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), - avatar: agent.username ? { - description: agent.username, - src: getAvatarUrl(agent.username), - } : undefined, - } : undefined} + agent={ + agent + ? { + _id: agent._id, + name: agent.name, + status: agent.status, + email: agent.emails && agent.emails[0] && agent.emails[0].address, + username: agent.username, + phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), + avatar: agent.username + ? { + description: agent.username, + src: getAvatarUrl(agent.username), + } + : undefined, + } + : undefined + } room={room} messages={messages && messages.filter((message) => canRenderMessage(message))} noMoreMessages={noMoreMessages} @@ -452,11 +454,15 @@ export const ChatConnector = ({ ref, t, ...props }) => ( lastReadMessageId={lastReadMessageId} guest={guest} triggerAgent={triggerAgent} - queueInfo={queueInfo ? { - spot: queueInfo.spot, - estimatedWaitTimeSeconds: queueInfo.estimatedWaitTimeSeconds, - message: queueInfo.message, - } : undefined} + queueInfo={ + queueInfo + ? { + spot: queueInfo.spot, + estimatedWaitTimeSeconds: queueInfo.estimatedWaitTimeSeconds, + message: queueInfo.message, + } + : undefined + } registrationFormEnabled={registrationForm} nameFieldRegistrationForm={nameFieldRegistrationForm} emailFieldRegistrationForm={emailFieldRegistrationForm} diff --git a/packages/livechat/src/routes/Chat/stories.js b/packages/livechat/src/routes/Chat/stories.js index f498321a6be..2ed307d65c3 100644 --- a/packages/livechat/src/routes/Chat/stories.js +++ b/packages/livechat/src/routes/Chat/stories.js @@ -6,7 +6,6 @@ import soundSrc from '../../../.storybook/assets/beep.mp3'; import { screenCentered, screenProps, avatarResolver } from '../../helpers.stories'; import Chat from './component'; - const agent = { name: 'Guilherme Gazzo', status: 'online', @@ -18,7 +17,11 @@ const agent = { const now = new Date(Date.parse('2021-01-01T00:00:00.000Z')); const messages = [ - { _id: 1, u: { _id: 1, username: 'tasso.evangelista' }, msg: 'Lorem ipsum dolor sit amet, ea usu quod eirmod lucilius, mea veri viris concludaturque id, vel eripuit fabulas ea' }, + { + _id: 1, + u: { _id: 1, username: 'tasso.evangelista' }, + msg: 'Lorem ipsum dolor sit amet, ea usu quod eirmod lucilius, mea veri viris concludaturque id, vel eripuit fabulas ea', + }, { _id: 2, u: { _id: 2, username: 'guilherme.gazzo' }, msg: 'Putent appareat te sea, dico recusabo pri te' }, { _id: 3, u: { _id: 2, username: 'guilherme.gazzo' }, msg: 'Iudico utinam volutpat eos eu, sadipscing repudiandae pro te' }, { _id: 4, u: { _id: 2, username: 'guilherme.gazzo' }, msg: 'Movet doming ad ius, mel id adversarium disputationi' }, @@ -34,10 +37,11 @@ const triggers = [ { _id: 2, u: { _id: 2, username: 'guilherme.gazzo' }, msg: 'Iudico utinam volutpat eos eu, sadipscing repudiandae pro te' }, ]; -const normalizeMessages = (messages = []) => messages.map((message, i) => ({ - ...message, - ts: new Date(now.getTime() - (15 - i) * 60000 - (i < 5 ? 24 * 60 * 60 * 1000 : 0)).toISOString(), -})); +const normalizeMessages = (messages = []) => + messages.map((message, i) => ({ + ...message, + ts: new Date(now.getTime() - (15 - i) * 60000 - (i < 5 ? 24 * 60 * 60 * 1000 : 0)).toISOString(), + })); storiesOf('Routes/Chat', module) .addDecorator(screenCentered) diff --git a/packages/livechat/src/routes/ChatFinished/component.js b/packages/livechat/src/routes/ChatFinished/component.js index b081187f53e..1fc4603cebc 100644 --- a/packages/livechat/src/routes/ChatFinished/component.js +++ b/packages/livechat/src/routes/ChatFinished/component.js @@ -26,22 +26,21 @@ class ChatFinished extends Component { const defaultGreeting = t('thanks_for_talking_with_us'); const defaultMessage = t('if_you_have_any_other_questions_just_press_the_but'); - return <Screen - color={color} - title={title} - className={createClassName(styles, 'chat-finished')} - {...props} - > - <Screen.Content> - <p className={createClassName(styles, 'chat-finished__greeting')}>{greeting || defaultGreeting}</p> - <p className={createClassName(styles, 'chat-finished__message')}>{message || defaultMessage}</p> + return ( + <Screen color={color} title={title} className={createClassName(styles, 'chat-finished')} {...props}> + <Screen.Content> + <p className={createClassName(styles, 'chat-finished__greeting')}>{greeting || defaultGreeting}</p> + <p className={createClassName(styles, 'chat-finished__message')}>{message || defaultMessage}</p> - <ButtonGroup> - <Button onClick={this.handleClick} stack>{ t('new_chat') }</Button> - </ButtonGroup> - </Screen.Content> - <Screen.Footer /> - </Screen>; + <ButtonGroup> + <Button onClick={this.handleClick} stack> + {t('new_chat')} + </Button> + </ButtonGroup> + </Screen.Content> + <Screen.Footer /> + </Screen> + ); }; } diff --git a/packages/livechat/src/routes/ChatFinished/container.js b/packages/livechat/src/routes/ChatFinished/container.js index a008f97c518..601bfe48e85 100644 --- a/packages/livechat/src/routes/ChatFinished/container.js +++ b/packages/livechat/src/routes/ChatFinished/container.js @@ -5,37 +5,19 @@ import { withTranslation } from 'react-i18next'; import { Consumer } from '../../store'; import ChatFinished from './component'; - class ChatFinishedContainer extends Component { handleRedirect = () => { route('/'); }; - render = (props) => ( - <ChatFinished {...props} onRedirectChat={this.handleRedirect} /> - ); + render = (props) => <ChatFinished {...props} onRedirectChat={this.handleRedirect} />; } - const ChatFinishedConnector = ({ ref, t, ...props }) => ( <Consumer> {({ - config: { - messages: { - conversationFinishedMessage: greeting, - conversationFinishedText: message, - } = {}, - theme: { - color, - } = {}, - } = {}, - iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - } = {}, - } = {}, + config: { messages: { conversationFinishedMessage: greeting, conversationFinishedText: message } = {}, theme: { color } = {} } = {}, + iframe: { theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = {} } = {}, }) => ( <ChatFinishedContainer ref={ref} @@ -53,5 +35,4 @@ const ChatFinishedConnector = ({ ref, t, ...props }) => ( </Consumer> ); - export default withTranslation()(ChatFinishedConnector); diff --git a/packages/livechat/src/routes/GDPRAgreement/component.js b/packages/livechat/src/routes/GDPRAgreement/component.js index 0014e57433f..dcfc155d7de 100644 --- a/packages/livechat/src/routes/GDPRAgreement/component.js +++ b/packages/livechat/src/routes/GDPRAgreement/component.js @@ -29,39 +29,36 @@ class GDPR extends Component { t, ...props }) => ( - <Screen - color={color} - title={title} - className={createClassName(styles, 'gdpr')} - {...props} - > + <Screen color={color} title={title} className={createClassName(styles, 'gdpr')} {...props}> <Screen.Content> - { - consentText - ? <p - className={createClassName(styles, 'gdpr__consent-text')} - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: md.renderInline(consentText) }} - /> - : <p className={createClassName(styles, 'gdpr__consent-text')}> - <Trans i18nKey='the_controller_of_your_personal_data_is_company_na' /> - </p> - } - { - instructions - ? <p - className={createClassName(styles, 'gdpr__instructions')} - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: md.renderInline(instructions) }} - /> - : <p className={createClassName(styles, 'gdpr__instructions')}> - <Trans i18nKey='go_to_menu_options_forget_remove_my_personal_data'> - Go to <strong>menu options → Forget/Remove my personal data</strong> to request the immediate removal of your data. - </Trans> - </p> - } + {consentText ? ( + <p + className={createClassName(styles, 'gdpr__consent-text')} + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ __html: md.renderInline(consentText) }} + /> + ) : ( + <p className={createClassName(styles, 'gdpr__consent-text')}> + <Trans i18nKey='the_controller_of_your_personal_data_is_company_na' /> + </p> + )} + {instructions ? ( + <p + className={createClassName(styles, 'gdpr__instructions')} + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ __html: md.renderInline(instructions) }} + /> + ) : ( + <p className={createClassName(styles, 'gdpr__instructions')}> + <Trans i18nKey='go_to_menu_options_forget_remove_my_personal_data'> + Go to <strong>menu options → Forget/Remove my personal data</strong> to request the immediate removal of your data. + </Trans> + </p> + )} <ButtonGroup> - <Button onClick={this.handleClick} stack>{ t('i_agree') }</Button> + <Button onClick={this.handleClick} stack> + {t('i_agree')} + </Button> </ButtonGroup> </Screen.Content> <Screen.Footer /> diff --git a/packages/livechat/src/routes/GDPRAgreement/container.js b/packages/livechat/src/routes/GDPRAgreement/container.js index ae38907215a..82c290eb55e 100644 --- a/packages/livechat/src/routes/GDPRAgreement/container.js +++ b/packages/livechat/src/routes/GDPRAgreement/container.js @@ -5,7 +5,6 @@ import { withTranslation } from 'react-i18next'; import { Consumer } from '../../store'; import GDPRAgreement from './component'; - class GDPRContainer extends Component { handleAgree = async () => { const { dispatch } = this.props; @@ -13,29 +12,14 @@ class GDPRContainer extends Component { route('/'); }; - render = (props) => ( - <GDPRAgreement {...props} onAgree={this.handleAgree} /> - ); + render = (props) => <GDPRAgreement {...props} onAgree={this.handleAgree} />; } const GDPRConnector = ({ ref, t, ...props }) => ( <Consumer> {({ - config: { - theme: { - color, - } = {}, - messages: { - dataProcessingConsentText: consentText, - } = {}, - } = {}, - iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - } = {}, - } = {}, + config: { theme: { color } = {}, messages: { dataProcessingConsentText: consentText } = {} } = {}, + iframe: { theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = {} } = {}, dispatch, }) => ( <GDPRContainer @@ -54,5 +38,4 @@ const GDPRConnector = ({ ref, t, ...props }) => ( </Consumer> ); - export default withTranslation()(GDPRConnector); diff --git a/packages/livechat/src/routes/GDPRAgreement/stories.js b/packages/livechat/src/routes/GDPRAgreement/stories.js index 72015760d08..7b5fa283473 100644 --- a/packages/livechat/src/routes/GDPRAgreement/stories.js +++ b/packages/livechat/src/routes/GDPRAgreement/stories.js @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { screenCentered, screenProps } from '../../helpers.stories'; import GDPRAgreement from './component'; - storiesOf('Routes/GDPRAgreement', module) .addDecorator(screenCentered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/routes/LeaveMessage/component.js b/packages/livechat/src/routes/LeaveMessage/component.js index f7597f1acef..ae891e7d522 100644 --- a/packages/livechat/src/routes/LeaveMessage/component.js +++ b/packages/livechat/src/routes/LeaveMessage/component.js @@ -38,9 +38,10 @@ class LeaveMessage extends Component { }; }; - getValidableFields = () => Object.keys(this.validations) - .map((fieldName) => (this.state[fieldName] ? { fieldName, ...this.state[fieldName] } : null)) - .filter(Boolean); + getValidableFields = () => + Object.keys(this.validations) + .map((fieldName) => (this.state[fieldName] ? { fieldName, ...this.state[fieldName] } : null)) + .filter(Boolean); validate = ({ name, value }) => this.validations[name].reduce((error, validation) => error || validation({ value }), undefined); @@ -55,10 +56,14 @@ class LeaveMessage extends Component { isValid = () => this.getValidableFields().every(({ error } = {}) => !error); - handleFieldChange = (name) => ({ target: { value } }) => { - const error = this.validate({ name, value }); - this.setState({ [name]: { ...this.state[name], value, error, showError: false } }, () => { this.validateAll(); }); - }; + handleFieldChange = + (name) => + ({ target: { value } }) => { + const error = this.validate({ name, value }); + this.setState({ [name]: { ...this.state[name], value, error, showError: false } }, () => { + this.validateAll(); + }); + }; handleNameChange = this.handleFieldChange('name'); @@ -89,84 +94,63 @@ class LeaveMessage extends Component { renderForm = ({ loading, departments, valid = this.isValid(), t }, { name, email, department, message }) => ( <Form onSubmit={this.handleSubmit}> - {name - ? ( - <FormField - required - label={t('name')} - error={name.showError && name.error} - > - <TextInput - name='name' - value={name.value} - placeholder={t('insert_your_field_here', { field: t('name') })} - disabled={loading} - onInput={this.handleNameChange} - /> - </FormField> - ) - : null} - - {email - ? ( - <FormField - required - label={t('Email')} - error={email.showError && email.error} - > - <TextInput - name='email' - value={email.value} - placeholder={t('insert_your_field_here', { field: t('email') })} - disabled={loading} - onInput={this.handleEmailChange} - /> - </FormField> - ) - : null} - - {department - ? ( - <FormField - label={t('i_need_help_with')} - error={department.showError && department.error} - > - <SelectInput - name='department' - value={department.value} - options={sortArrayByColumn(departments, 'name').map(({ _id, name }) => ({ value: _id, label: name }))} - placeholder={t('choose_an_option')} - disabled={loading} - error={department.showError} - onInput={this.handleDepartmentChange} - /> - </FormField> - ) - : null} - - {message - ? ( - <FormField - required - label={t('message')} - error={message.showError && message.error} - > - <TextInput - name='message' - value={message.value} - multiline - rows={4} - placeholder={t('write_your_message')} - disabled={loading} - error={message.showError} - onInput={this.handleMessageChange} - /> - </FormField> - ) - : null} + {name ? ( + <FormField required label={t('name')} error={name.showError && name.error}> + <TextInput + name='name' + value={name.value} + placeholder={t('insert_your_field_here', { field: t('name') })} + disabled={loading} + onInput={this.handleNameChange} + /> + </FormField> + ) : null} + + {email ? ( + <FormField required label={t('Email')} error={email.showError && email.error}> + <TextInput + name='email' + value={email.value} + placeholder={t('insert_your_field_here', { field: t('email') })} + disabled={loading} + onInput={this.handleEmailChange} + /> + </FormField> + ) : null} + + {department ? ( + <FormField label={t('i_need_help_with')} error={department.showError && department.error}> + <SelectInput + name='department' + value={department.value} + options={sortArrayByColumn(departments, 'name').map(({ _id, name }) => ({ value: _id, label: name }))} + placeholder={t('choose_an_option')} + disabled={loading} + error={department.showError} + onInput={this.handleDepartmentChange} + /> + </FormField> + ) : null} + + {message ? ( + <FormField required label={t('message')} error={message.showError && message.error}> + <TextInput + name='message' + value={message.value} + multiline + rows={4} + placeholder={t('write_your_message')} + disabled={loading} + error={message.showError} + onInput={this.handleMessageChange} + /> + </FormField> + ) : null} <ButtonGroup> - <Button submit loading={loading} disabled={!valid || loading} stack>{t('send')}</Button> + <Button submit loading={loading} disabled={!valid || loading} stack> + {t('send')} + </Button> </ButtonGroup> </Form> ); @@ -176,21 +160,21 @@ class LeaveMessage extends Component { const defaultMessage = t('we_are_not_online_right_now_please_leave_a_message'); const defaultUnavailableMessage = ''; // TODO - return <Screen - color={color} - title={title || defaultTitle} - className={createClassName(styles, 'leave-message')} - {...props} - > - <Screen.Content> - <div className={createClassName(styles, 'leave-message__main-message')} - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: renderMarkdown(hasForm ? message || defaultMessage : unavailableMessage || defaultUnavailableMessage) }} - /> - {hasForm && this.renderForm(this.props, this.state)} - </Screen.Content> - <Screen.Footer /> - </Screen>; + return ( + <Screen color={color} title={title || defaultTitle} className={createClassName(styles, 'leave-message')} {...props}> + <Screen.Content> + <div + className={createClassName(styles, 'leave-message__main-message')} + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ + __html: renderMarkdown(hasForm ? message || defaultMessage : unavailableMessage || defaultUnavailableMessage), + }} + /> + {hasForm && this.renderForm(this.props, this.state)} + </Screen.Content> + <Screen.Footer /> + </Screen> + ); }; } diff --git a/packages/livechat/src/routes/LeaveMessage/container.js b/packages/livechat/src/routes/LeaveMessage/container.js index d31efb17168..31a4e60ad0e 100644 --- a/packages/livechat/src/routes/LeaveMessage/container.js +++ b/packages/livechat/src/routes/LeaveMessage/container.js @@ -8,7 +8,6 @@ import { createToken } from '../../lib/random'; import { Consumer } from '../../store'; import LeaveMessage from './component'; - export class LeaveMessageContainer extends Component { handleSubmit = async (fields) => { const { alerts, dispatch, successMessage } = this.props; @@ -23,7 +22,9 @@ export class LeaveMessageContainer extends Component { parentCall('callback', ['offline-form-submit', fields]); return true; } catch (error) { - const { data: { message } } = error; + const { + data: { message }, + } = error; console.error(message); const alert = { id: createToken(), children: message, error: true, timeout: 5000 }; await dispatch({ alerts: (alerts.push(alert), alerts) }); @@ -33,35 +34,19 @@ export class LeaveMessageContainer extends Component { } }; - render = (props) => ( - <LeaveMessage {...props} onSubmit={this.handleSubmit} /> - ); + render = (props) => <LeaveMessage {...props} onSubmit={this.handleSubmit} />; } - export const LeaveMessageConnector = ({ ref, ...props }) => ( <Consumer> {({ config: { departments = {}, - messages: { - offlineMessage: message, - offlineSuccessMessage: successMessage, - offlineUnavailableMessage: unavailableMessage, - } = {}, - theme: { - offlineTitle: title, - offlineColor: color, - } = {}, - settings: { - displayOfflineForm, - } = {}, - } = {}, - iframe: { - theme: { - offlineTitle: customOfflineTitle, - } = {}, + messages: { offlineMessage: message, offlineSuccessMessage: successMessage, offlineUnavailableMessage: unavailableMessage } = {}, + theme: { offlineTitle: title, offlineColor: color } = {}, + settings: { displayOfflineForm } = {}, } = {}, + iframe: { theme: { offlineTitle: customOfflineTitle } = {} } = {}, loading, token, dispatch, @@ -90,5 +75,4 @@ export const LeaveMessageConnector = ({ ref, ...props }) => ( </Consumer> ); - export default LeaveMessageConnector; diff --git a/packages/livechat/src/routes/LeaveMessage/stories.js b/packages/livechat/src/routes/LeaveMessage/stories.js index 895fd63a558..14fcd932004 100644 --- a/packages/livechat/src/routes/LeaveMessage/stories.js +++ b/packages/livechat/src/routes/LeaveMessage/stories.js @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { screenCentered, screenProps } from '../../helpers.stories'; import LeaveMessage from './component'; - storiesOf('Routes/Leave a message', module) .addDecorator(screenCentered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/routes/Register/component.js b/packages/livechat/src/routes/Register/component.js index 941809b049b..60cb1fda37e 100644 --- a/packages/livechat/src/routes/Register/component.js +++ b/packages/livechat/src/routes/Register/component.js @@ -3,57 +3,46 @@ import { withTranslation } from 'react-i18next'; import { Button } from '../../components/Button'; import { ButtonGroup } from '../../components/ButtonGroup'; -import { - Form, - FormField, - TextInput, - SelectInput, - Validations, -} from '../../components/Form'; +import { Form, FormField, TextInput, SelectInput, Validations } from '../../components/Form'; import Screen from '../../components/Screen'; import { createClassName, sortArrayByColumn } from '../../components/helpers'; import styles from './styles.scss'; const getDefaultDepartment = (departments = []) => (departments.length === 1 && departments[0]._id) || ''; -const renderCustomFields = (customFields, { loading, handleFieldChange = () => {} }, state, t) => customFields.map(({ _id, required, label, type, options }) => { - switch (type) { - case 'input': - return <FormField - label={label} - required={required} - key={_id} - error={state[_id].showError && state[_id].error} - > - <TextInput - name={_id} - placeholder={t('insert_your_field_here', { field: label })} - value={state[_id].value} - disabled={loading} - onInput={handleFieldChange} - custom - /> - </FormField>; - case 'select': - return <FormField - label={label} - required={required} - key={_id} - error={state[_id].showError && state[_id].error} - > - <SelectInput - name={_id} - value={state[_id].value} - placeholder={t('choose_an_option')} - options={options && options.map((option) => ({ value: option, label: option }))} - disabled={loading} - onInput={handleFieldChange} - custom - /> - </FormField>; - } - return null; -}); +const renderCustomFields = (customFields, { loading, handleFieldChange = () => {} }, state, t) => + customFields.map(({ _id, required, label, type, options }) => { + switch (type) { + case 'input': + return ( + <FormField label={label} required={required} key={_id} error={state[_id].showError && state[_id].error}> + <TextInput + name={_id} + placeholder={t('insert_your_field_here', { field: label })} + value={state[_id].value} + disabled={loading} + onInput={handleFieldChange} + custom + /> + </FormField> + ); + case 'select': + return ( + <FormField label={label} required={required} key={_id} error={state[_id].showError && state[_id].error}> + <SelectInput + name={_id} + value={state[_id].value} + placeholder={t('choose_an_option')} + options={options && options.map((option) => ({ value: option, label: option }))} + disabled={loading} + onInput={handleFieldChange} + custom + /> + </FormField> + ); + } + return null; + }); const validations = { name: [Validations.nonEmpty], @@ -94,9 +83,9 @@ const getDefaultState = (props) => { const { hasNameField, hasEmailField, hasDepartmentField, departments, customFields = [] } = props; let state = { - ...hasNameField && { name: { value: '' } }, - ...hasEmailField && { email: { value: '' } }, - ...hasDepartmentField && { department: { value: getDefaultDepartment(departments) } }, + ...(hasNameField && { name: { value: '' } }), + ...(hasEmailField && { email: { value: '' } }), + ...(hasDepartmentField && { department: { value: getDefaultDepartment(departments) } }), }; customFields.forEach(({ _id, defaultValue, options, regexp }) => { @@ -108,7 +97,7 @@ const getDefaultState = (props) => { state[_id] = { value, - ...regexp && { regexp }, + ...(regexp && { regexp }), error, showError: false, }; @@ -206,74 +195,54 @@ class Register extends Component { const valid = getValidableFields(this.state).every(({ error } = {}) => !error); return ( - <Screen - color={color} - title={title || defaultTitle} - className={createClassName(styles, 'register')} - {...props} - > + <Screen color={color} title={title || defaultTitle} className={createClassName(styles, 'register')} {...props}> <Screen.Content> <p className={createClassName(styles, 'register__message')}>{message || defaultMessage}</p> <Form onSubmit={this.handleSubmit}> - {name - ? ( - <FormField - required - label={t('name')} - error={name.showError && name.error} - > - <TextInput - name='name' - value={name.value} - placeholder={t('insert_your_field_here', { field: t('name') })} - disabled={loading} - onInput={this.handleFieldChange} - /> - </FormField> - ) - : null} - - {email - ? ( - <FormField - required - label={t('email')} - error={email.showError && email.error} - > - <TextInput - name='email' - value={email.value} - placeholder={t('insert_your_field_here', { field: t('email') })} - disabled={loading} - onInput={this.handleFieldChange} - /> - </FormField> - ) - : null} - - {department - ? ( - <FormField - label={t('i_need_help_with')} - error={department.showError && department.error} - > - <SelectInput - name='department' - value={department.value} - options={sortArrayByColumn(departments, 'name').map(({ _id, name }) => ({ value: _id, label: name }))} - placeholder={t('choose_an_option')} - disabled={loading} - onInput={this.handleFieldChange} - /> - </FormField> - ) - : null} + {name ? ( + <FormField required label={t('name')} error={name.showError && name.error}> + <TextInput + name='name' + value={name.value} + placeholder={t('insert_your_field_here', { field: t('name') })} + disabled={loading} + onInput={this.handleFieldChange} + /> + </FormField> + ) : null} + + {email ? ( + <FormField required label={t('email')} error={email.showError && email.error}> + <TextInput + name='email' + value={email.value} + placeholder={t('insert_your_field_here', { field: t('email') })} + disabled={loading} + onInput={this.handleFieldChange} + /> + </FormField> + ) : null} + + {department ? ( + <FormField label={t('i_need_help_with')} error={department.showError && department.error}> + <SelectInput + name='department' + value={department.value} + options={sortArrayByColumn(departments, 'name').map(({ _id, name }) => ({ value: _id, label: name }))} + placeholder={t('choose_an_option')} + disabled={loading} + onInput={this.handleFieldChange} + /> + </FormField> + ) : null} {customFields && renderCustomFields(customFields, { loading, handleFieldChange: this.handleFieldChange }, state, t)} <ButtonGroup> - <Button submit loading={loading} disabled={!valid || loading} stack>{t('start_chat')}</Button> + <Button submit loading={loading} disabled={!valid || loading} stack> + {t('start_chat')} + </Button> </ButtonGroup> </Form> </Screen.Content> diff --git a/packages/livechat/src/routes/Register/container.js b/packages/livechat/src/routes/Register/container.js index 24a1f3c4126..4a8b3672a6c 100644 --- a/packages/livechat/src/routes/Register/container.js +++ b/packages/livechat/src/routes/Register/container.js @@ -66,9 +66,7 @@ export class RegisterContainer extends Component { } } - render = (props) => ( - <Register {...props} onSubmit={this.handleSubmit} departmentDefault={this.getDepartmentDefault()} /> - ); + render = (props) => <Register {...props} onSubmit={this.handleSubmit} departmentDefault={this.getDepartmentDefault()} />; } export const RegisterConnector = ({ ref, ...props }) => ( @@ -76,31 +74,14 @@ export const RegisterConnector = ({ ref, ...props }) => ( {({ config: { departments = {}, - messages: { - registrationFormMessage: message, - } = {}, - settings: { - nameFieldRegistrationForm: hasNameField, - emailFieldRegistrationForm: hasEmailField, - } = {}, - theme: { - title, - color, - } = {}, + messages: { registrationFormMessage: message } = {}, + settings: { nameFieldRegistrationForm: hasNameField, emailFieldRegistrationForm: hasEmailField } = {}, + theme: { title, color } = {}, customFields = [], } = {}, iframe: { - guest: { - department: guestDepartment, - name: guestName, - email: guestEmail, - } = {}, - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - title: customTitle, - } = {}, + guest: { department: guestDepartment, name: guestName, email: guestEmail } = {}, + theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor, title: customTitle } = {}, } = {}, loading = false, token, @@ -135,5 +116,4 @@ export const RegisterConnector = ({ ref, ...props }) => ( </Consumer> ); - export default RegisterConnector; diff --git a/packages/livechat/src/routes/Register/stories.js b/packages/livechat/src/routes/Register/stories.js index cf75875c97f..014d0cfeb4d 100644 --- a/packages/livechat/src/routes/Register/stories.js +++ b/packages/livechat/src/routes/Register/stories.js @@ -11,7 +11,8 @@ const customFields = [ label: 'Website', type: 'input', required: true, - }, { + }, + { _id: 'area', label: 'Area', type: 'select', diff --git a/packages/livechat/src/routes/SwitchDepartment/component.js b/packages/livechat/src/routes/SwitchDepartment/component.js index cdbc5800355..2bf854d6156 100644 --- a/packages/livechat/src/routes/SwitchDepartment/component.js +++ b/packages/livechat/src/routes/SwitchDepartment/component.js @@ -29,9 +29,10 @@ class SwitchDepartment extends Component { department: [Validations.nonEmpty], }; - getValidableFields = () => Object.keys(this.validations) - .map((fieldName) => (this.state[fieldName] ? { fieldName, ...this.state[fieldName] } : null)) - .filter(Boolean); + getValidableFields = () => + Object.keys(this.validations) + .map((fieldName) => (this.state[fieldName] ? { fieldName, ...this.state[fieldName] } : null)) + .filter(Boolean); validate = ({ name, value }) => this.validations[name].reduce((error, validation) => error || validation({ value }), undefined); @@ -44,10 +45,14 @@ class SwitchDepartment extends Component { isValid = () => this.getValidableFields().every(({ error } = {}) => !error); - handleFieldChange = (name) => ({ target: { value } }) => { - const error = this.validate({ name, value }); - this.setState({ [name]: { ...this.state[name], value, error, showError: false } }, () => { this.validateAll(); }); - }; + handleFieldChange = + (name) => + ({ target: { value } }) => { + const error = this.validate({ name, value }); + this.setState({ [name]: { ...this.state[name], value, error, showError: false } }, () => { + this.validateAll(); + }); + }; handleDepartmentChange = this.handleFieldChange('department'); @@ -88,20 +93,12 @@ class SwitchDepartment extends Component { const valid = this.isValid(); return ( - <Screen - color={color} - title={title || defaultTitle} - className={createClassName(styles, 'switch-department')} - {...props} - > + <Screen color={color} title={title || defaultTitle} className={createClassName(styles, 'switch-department')} {...props}> <Screen.Content> <p className={createClassName(styles, 'switch-department__message')}>{message || defaultMessage}</p> <Form onSubmit={this.handleSubmit}> - <FormField - label={t('departments')} - error={department && department.showError && department.error} - > + <FormField label={t('departments')} error={department && department.showError && department.error}> <SelectInput name='department' value={department && department.value} @@ -114,8 +111,12 @@ class SwitchDepartment extends Component { </FormField> <ButtonGroup> - <Button submit loading={loading} disabled={!valid || loading} stack>{t('start_chat')}</Button> - <Button disabled={loading} stack secondary onClick={this.handleCancelClick}>{t('cancel')}</Button> + <Button submit loading={loading} disabled={!valid || loading} stack> + {t('start_chat')} + </Button> + <Button disabled={loading} stack secondary onClick={this.handleCancelClick}> + {t('cancel')} + </Button> </ButtonGroup> </Form> </Screen.Content> diff --git a/packages/livechat/src/routes/SwitchDepartment/connector.js b/packages/livechat/src/routes/SwitchDepartment/connector.js index 42d5a54980d..41679b15a35 100644 --- a/packages/livechat/src/routes/SwitchDepartment/connector.js +++ b/packages/livechat/src/routes/SwitchDepartment/connector.js @@ -4,20 +4,8 @@ import SwitchDepartmentContainer from './container'; const SwitchDepartmentConnector = ({ ref, ...props }) => ( <Consumer> {({ - config: { - departments = {}, - theme: { - color, - } = {}, - } = {}, - iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - } = {}, - guest, - } = {}, + config: { departments = {}, theme: { color } = {} } = {}, + iframe: { theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = {}, guest } = {}, iframe, room, loading = false, diff --git a/packages/livechat/src/routes/SwitchDepartment/container.js b/packages/livechat/src/routes/SwitchDepartment/container.js index c47bcf9b8c2..e4932053b91 100644 --- a/packages/livechat/src/routes/SwitchDepartment/container.js +++ b/packages/livechat/src/routes/SwitchDepartment/container.js @@ -52,7 +52,9 @@ class SwitchDepartmentContainer extends Component { route('/'); } catch (error) { console.error(error); - await dispatch({ alerts: (alerts.push({ id: createToken(), children: t('no_available_agents_to_transfer'), warning: true }), alerts) }); + await dispatch({ + alerts: (alerts.push({ id: createToken(), children: t('no_available_agents_to_transfer'), warning: true }), alerts), + }); } finally { await dispatch({ loading: false }); } @@ -62,9 +64,7 @@ class SwitchDepartmentContainer extends Component { route('/'); }; - render = (props) => ( - <SwitchDepartment {...props} onSubmit={this.handleSubmit} onCancel={this.handleCancel} /> - ); + render = (props) => <SwitchDepartment {...props} onSubmit={this.handleSubmit} onCancel={this.handleCancel} />; } export default withTranslation()(SwitchDepartmentContainer); diff --git a/packages/livechat/src/routes/SwitchDepartment/stories.js b/packages/livechat/src/routes/SwitchDepartment/stories.js index 0301c2b3b76..03584ad0d80 100644 --- a/packages/livechat/src/routes/SwitchDepartment/stories.js +++ b/packages/livechat/src/routes/SwitchDepartment/stories.js @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { screenCentered, screenProps } from '../../helpers.stories'; import SwitchDepartment from './component'; - storiesOf('Routes/SwitchDepartment', module) .addDecorator(screenCentered) .addDecorator(withKnobs) diff --git a/packages/livechat/src/routes/TriggerMessage/component.js b/packages/livechat/src/routes/TriggerMessage/component.js index 0c66050bdd7..857504dd409 100644 --- a/packages/livechat/src/routes/TriggerMessage/component.js +++ b/packages/livechat/src/routes/TriggerMessage/component.js @@ -6,9 +6,8 @@ import { createClassName } from '../../components/helpers'; import { parentCall } from '../../lib/parentCall'; import styles from './styles.scss'; - class TriggerMessage extends Component { - state = { }; + state = {}; constructor(props) { super(props); @@ -27,20 +26,20 @@ class TriggerMessage extends Component { render({ title, messages, loading, onStartChat = () => {}, departments, t, ...props }) { const defaultTitle = t('messages'); - const { theme: { color } } = props; + const { + theme: { color }, + } = props; return ( - <Screen - title={title || defaultTitle} - {...props} - triggered={true} - ref={this.ref} - > + <Screen title={title || defaultTitle} {...props} triggered={true} ref={this.ref}> <Screen.Content triggered={true}> - {messages && messages.map((message) => message.msg && <p className={createClassName(styles, 'trigger-message__message')}>{message.msg}</p>)} + {messages && + messages.map((message) => message.msg && <p className={createClassName(styles, 'trigger-message__message')}>{message.msg}</p>)} </Screen.Content> <footer className={createClassName(styles, 'trigger-message__footer')}> <hr className={createClassName(styles, 'trigger-message__separator')} /> - <button style={color && { color }} onClick={onStartChat} className={createClassName(styles, 'trigger-message__link-reply')}>{t('start_chat')}</button> + <button style={color && { color }} onClick={onStartChat} className={createClassName(styles, 'trigger-message__link-reply')}> + {t('start_chat')} + </button> </footer> </Screen> ); diff --git a/packages/livechat/src/routes/TriggerMessage/container.js b/packages/livechat/src/routes/TriggerMessage/container.js index daf19a9d63a..e77ac70c1d0 100644 --- a/packages/livechat/src/routes/TriggerMessage/container.js +++ b/packages/livechat/src/routes/TriggerMessage/container.js @@ -6,7 +6,6 @@ import { parentCall } from '../../lib/parentCall'; import { Consumer } from '../../store'; import TriggerMessage from './component'; - export class TriggerMessageContainer extends Component { handleStart(props) { parentCall('setFullScreenDocumentMobile'); @@ -24,18 +23,8 @@ export class TriggerMessageContainer extends Component { export const TriggerMessageConnector = ({ ref, ...props }) => ( <Consumer> {({ - config: { - theme: { - color, - } = {}, - } = {}, - iframe: { - theme: { - color: customColor, - fontColor: customFontColor, - iconColor: customIconColor, - } = {}, - } = {}, + config: { theme: { color } = {} } = {}, + iframe: { theme: { color: customColor, fontColor: customFontColor, iconColor: customIconColor } = {} } = {}, messages, agent, unread, @@ -49,18 +38,24 @@ export const TriggerMessageConnector = ({ ref, ...props }) => ( iconColor: customIconColor, }} unread={unread} - agent={agent ? { - _id: agent._id, - name: agent.name, - status: agent.status, - email: agent.emails && agent.emails[0] && agent.emails[0].address, - username: agent.username, - phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), - avatar: agent.username ? { - description: agent.username, - src: getAvatarUrl(agent.username), - } : undefined, - } : undefined} + agent={ + agent + ? { + _id: agent._id, + name: agent.name, + status: agent.status, + email: agent.emails && agent.emails[0] && agent.emails[0].address, + username: agent.username, + phone: (agent.phone && agent.phone[0] && agent.phone[0].phoneNumber) || (agent.customFields && agent.customFields.phone), + avatar: agent.username + ? { + description: agent.username, + src: getAvatarUrl(agent.username), + } + : undefined, + } + : undefined + } messages={messages && messages.filter((message) => canRenderMessage(message))} /> )} diff --git a/packages/livechat/src/routes/TriggerMessage/stories.js b/packages/livechat/src/routes/TriggerMessage/stories.js index 8d18c214de5..0fe12f21096 100644 --- a/packages/livechat/src/routes/TriggerMessage/stories.js +++ b/packages/livechat/src/routes/TriggerMessage/stories.js @@ -9,7 +9,11 @@ const now = new Date(Date.parse('2021-01-01T00:00:00.000Z')); const messages = [ { _id: 1, u: { _id: 1, username: 'guilherme.gazzo' }, msg: 'Hi There!' }, - { _id: 2, u: { _id: 2, username: 'guilherme.gazzo' }, msg: 'Rocket.Chat allows you to chat and create better relationship with your customers on their favorite channels. ' }, + { + _id: 2, + u: { _id: 2, username: 'guilherme.gazzo' }, + msg: 'Rocket.Chat allows you to chat and create better relationship with your customers on their favorite channels. ', + }, { _id: 3, u: { _id: 3, username: 'guilherme.gazzo' }, msg: 'Let us know if you have any question.' }, ].map((message, i) => ({ ...message, @@ -26,7 +30,10 @@ storiesOf('Routes/TriggerMessage', module) fontColor: color('theme/fontColor', ''), iconColor: color('theme/iconColor', ''), }} - messages={object('messages', messages.filter(({ _id }) => _id === 3))} + messages={object( + 'messages', + messages.filter(({ _id }) => _id === 3), + )} title={text('title', '')} onSubmit={action('submit')} onCancel={action('cancel')} diff --git a/packages/livechat/src/store/index.js b/packages/livechat/src/store/index.js index 21eba236b7e..e71e9022534 100644 --- a/packages/livechat/src/store/index.js +++ b/packages/livechat/src/store/index.js @@ -34,7 +34,7 @@ export const initialState = () => ({ minimized: true, unread: null, incomingCallAlert: null, - ongoingCall: null, // TODO: store call info like url, startTime, timeout, etc here + ongoingCall: null, // TODO: store call info like url, startTime, timeout, etc here businessUnit: null, }); @@ -48,7 +48,6 @@ if (process.env.NODE_ENV === 'development') { }); } - const StoreContext = createContext(); export class Provider extends Component { @@ -68,11 +67,7 @@ export class Provider extends Component { store.off('change', this.handleStoreChange); } - render = ({ children }) => ( - <StoreContext.Provider value={this.state}> - {children} - </StoreContext.Provider> - ); + render = ({ children }) => <StoreContext.Provider value={this.state}>{children}</StoreContext.Provider>; } export const { Consumer } = StoreContext; diff --git a/packages/livechat/src/widget.js b/packages/livechat/src/widget.js index 52ed4af994f..a8bdbab6217 100644 --- a/packages/livechat/src/widget.js +++ b/packages/livechat/src/widget.js @@ -1,10 +1,9 @@ import mitt from 'mitt'; - -const log = process.env.NODE_ENV === 'development' - ? (...args) => window.console.log('%cwidget%c', 'color: red', 'color: initial', ...args) - : () => {}; - +const log = + process.env.NODE_ENV === 'development' + ? (...args) => window.console.log('%cwidget%c', 'color: red', 'color: initial', ...args) + : () => {}; const WIDGET_OPEN_WIDTH = 365; const WIDGET_OPEN_HEIGHT = 525; @@ -12,7 +11,6 @@ const WIDGET_MINIMIZED_WIDTH = 54; const WIDGET_MINIMIZED_HEIGHT = 54; const WIDGET_MARGIN = 16; - window.RocketChat = window.RocketChat || { _: [] }; const config = {}; let widget; @@ -82,7 +80,6 @@ const updateWidgetStyle = (isOpened) => { } } - if (isOpened) { widget.style.left = isFullscreen ? '0' : 'auto'; @@ -94,12 +91,12 @@ const updateWidgetStyle = (isOpened) => { * for widget.style.width */ - widget.style.height = isFullscreen ? '100%' : `${ WIDGET_MARGIN + widget_height + WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT }px`; - widget.style.width = isFullscreen ? '100%' : `${ WIDGET_MARGIN + WIDGET_OPEN_WIDTH + WIDGET_MARGIN }px`; + widget.style.height = isFullscreen ? '100%' : `${WIDGET_MARGIN + widget_height + WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT}px`; + widget.style.width = isFullscreen ? '100%' : `${WIDGET_MARGIN + WIDGET_OPEN_WIDTH + WIDGET_MARGIN}px`; } else { widget.style.left = 'auto'; - widget.style.width = `${ WIDGET_MARGIN + WIDGET_MINIMIZED_WIDTH + WIDGET_MARGIN }px`; - widget.style.height = `${ WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT + WIDGET_MARGIN }px`; + widget.style.width = `${WIDGET_MARGIN + WIDGET_MINIMIZED_WIDTH + WIDGET_MARGIN}px`; + widget.style.height = `${WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT + WIDGET_MARGIN}px`; } }; @@ -107,8 +104,8 @@ const createWidget = (url) => { widget = document.createElement('div'); widget.className = 'rocketchat-widget'; widget.style.position = 'fixed'; - widget.style.width = `${ WIDGET_MARGIN + WIDGET_MINIMIZED_WIDTH + WIDGET_MARGIN }px`; - widget.style.height = `${ WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT + WIDGET_MARGIN }px`; + widget.style.width = `${WIDGET_MARGIN + WIDGET_MINIMIZED_WIDTH + WIDGET_MARGIN}px`; + widget.style.height = `${WIDGET_MARGIN + WIDGET_MINIMIZED_HEIGHT + WIDGET_MARGIN}px`; widget.style.maxHeight = '100vh'; widget.style.bottom = '0'; widget.style.right = '0'; @@ -182,7 +179,7 @@ const api = { ready() { ready = true; if (hookQueue.length > 0) { - hookQueue.forEach(function(hookParams) { + hookQueue.forEach(function (hookParams) { callHook.apply(this, hookParams); }); hookQueue = []; @@ -203,8 +200,11 @@ const api = { openPopout() { closeWidget(); - api.popup = window.open(`${ config.url }${ config.url.lastIndexOf('?') > -1 ? '&' : '?' }mode=popout`, - 'livechat-popout', `width=${ WIDGET_OPEN_WIDTH }, height=${ widget_height }, toolbars=no`); + api.popup = window.open( + `${config.url}${config.url.lastIndexOf('?') > -1 ? '&' : '?'}mode=popout`, + 'livechat-popout', + `width=${WIDGET_OPEN_WIDTH}, height=${widget_height}, toolbars=no`, + ); api.popup.focus(); }, @@ -379,15 +379,19 @@ const currentPage = { }; const attachMessageListener = () => { - window.addEventListener('message', (msg) => { - if (typeof msg.data === 'object' && msg.data.src !== undefined && msg.data.src === 'rocketchat') { - if (api[msg.data.fn] !== undefined && typeof api[msg.data.fn] === 'function') { - const args = [].concat(msg.data.args || []); - log(`api.${ msg.data.fn }`, ...args); - api[msg.data.fn].apply(null, args); + window.addEventListener( + 'message', + (msg) => { + if (typeof msg.data === 'object' && msg.data.src !== undefined && msg.data.src === 'rocketchat') { + if (api[msg.data.fn] !== undefined && typeof api[msg.data.fn] === 'function') { + const args = [].concat(msg.data.args || []); + log(`api.${msg.data.fn}`, ...args); + api[msg.data.fn].apply(null, args); + } } - } - }, false); + }, + false, + ); }; const trackNavigation = () => { @@ -427,7 +431,7 @@ if (typeof window.RocketChat.url !== 'undefined') { const queue = window.RocketChat._; -window.RocketChat._.push = function(c) { +window.RocketChat._.push = function (c) { c.call(window.RocketChat.livechat); }; window.RocketChat = window.RocketChat._.push; @@ -455,18 +459,42 @@ window.RocketChat.livechat = { clearBusinessUnit, // callbacks - onChatMaximized(fn) { registerCallback('chat-maximized', fn); }, - onChatMinimized(fn) { registerCallback('chat-minimized', fn); }, - onChatStarted(fn) { registerCallback('chat-started', fn); }, - onChatEnded(fn) { registerCallback('chat-ended', fn); }, - onPrechatFormSubmit(fn) { registerCallback('pre-chat-form-submit', fn); }, - onOfflineFormSubmit(fn) { registerCallback('offline-form-submit', fn); }, - onWidgetShown(fn) { registerCallback('show-widget', fn); }, - onWidgetHidden(fn) { registerCallback('hide-widget', fn); }, - onAssignAgent(fn) { registerCallback('assign-agent', fn); }, - onAgentStatusChange(fn) { registerCallback('agent-status-change', fn); }, - onQueuePositionChange(fn) { registerCallback('queue-position-change', fn); }, - onServiceOffline(fn) { registerCallback('no-agent-online', fn); }, + onChatMaximized(fn) { + registerCallback('chat-maximized', fn); + }, + onChatMinimized(fn) { + registerCallback('chat-minimized', fn); + }, + onChatStarted(fn) { + registerCallback('chat-started', fn); + }, + onChatEnded(fn) { + registerCallback('chat-ended', fn); + }, + onPrechatFormSubmit(fn) { + registerCallback('pre-chat-form-submit', fn); + }, + onOfflineFormSubmit(fn) { + registerCallback('offline-form-submit', fn); + }, + onWidgetShown(fn) { + registerCallback('show-widget', fn); + }, + onWidgetHidden(fn) { + registerCallback('hide-widget', fn); + }, + onAssignAgent(fn) { + registerCallback('assign-agent', fn); + }, + onAgentStatusChange(fn) { + registerCallback('agent-status-change', fn); + }, + onQueuePositionChange(fn) { + registerCallback('queue-position-change', fn); + }, + onServiceOffline(fn) { + registerCallback('no-agent-online', fn); + }, }; // proccess queue -- GitLab From f8294efa9fe0e3d1c66d1a7bf11d5d4adc6aced2 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:36:03 +0530 Subject: [PATCH 023/107] Chore: Deprecate some omnichannel meteor methods which aren't getting used (#26839) --- apps/meteor/app/livechat/server/methods/addAgent.js | 2 ++ apps/meteor/app/livechat/server/methods/addManager.js | 2 ++ apps/meteor/app/livechat/server/methods/closeByVisitor.js | 2 ++ apps/meteor/app/livechat/server/methods/getCustomFields.js | 3 +++ .../server/methods/getDepartmentForwardRestrictions.js | 2 ++ .../meteor/app/livechat/server/methods/getFirstRoomMessage.js | 2 ++ apps/meteor/app/livechat/server/methods/getNextAgent.js | 2 ++ apps/meteor/app/livechat/server/methods/getTagsList.js | 2 ++ apps/meteor/app/livechat/server/methods/loadHistory.js | 3 +++ apps/meteor/app/livechat/server/methods/loginByToken.js | 3 +++ apps/meteor/app/livechat/server/methods/pageVisited.js | 2 ++ apps/meteor/app/livechat/server/methods/registerGuest.js | 3 +++ apps/meteor/app/livechat/server/methods/removeAgent.js | 2 ++ apps/meteor/app/livechat/server/methods/removeDepartment.js | 3 +++ apps/meteor/app/livechat/server/methods/removeManager.js | 3 +++ .../app/livechat/server/methods/saveDepartmentAgents.js | 3 +++ apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js | 4 ++++ apps/meteor/app/livechat/server/methods/searchAgent.js | 3 +++ apps/meteor/app/livechat/server/methods/sendOfflineMessage.js | 3 +++ apps/meteor/app/livechat/server/methods/setCustomField.js | 3 +++ .../app/livechat/server/methods/setDepartmentForVisitor.js | 3 +++ 21 files changed, 55 insertions(+) diff --git a/apps/meteor/app/livechat/server/methods/addAgent.js b/apps/meteor/app/livechat/server/methods/addAgent.js index f391d2ed283..dd9b49b42ad 100644 --- a/apps/meteor/app/livechat/server/methods/addAgent.js +++ b/apps/meteor/app/livechat/server/methods/addAgent.js @@ -1,10 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ 'livechat:addAgent'(username) { + methodDeprecationLogger.warn('livechat:addAgent will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-agents')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addAgent' }); } diff --git a/apps/meteor/app/livechat/server/methods/addManager.js b/apps/meteor/app/livechat/server/methods/addManager.js index 3ffddfe0762..d365f3f6f4e 100644 --- a/apps/meteor/app/livechat/server/methods/addManager.js +++ b/apps/meteor/app/livechat/server/methods/addManager.js @@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:addManager'(username) { + methodDeprecationLogger.warn('livechat:addManager will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-managers')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addManager' }); } diff --git a/apps/meteor/app/livechat/server/methods/closeByVisitor.js b/apps/meteor/app/livechat/server/methods/closeByVisitor.js index 022e614bb7f..525656fc77d 100644 --- a/apps/meteor/app/livechat/server/methods/closeByVisitor.js +++ b/apps/meteor/app/livechat/server/methods/closeByVisitor.js @@ -5,9 +5,11 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:closeByVisitor'({ roomId, token }) { + methodDeprecationLogger.warn('livechat:closeByVisitor will be deprecated in future versions of Rocket.Chat'); const visitor = await LivechatVisitors.getVisitorByToken(token); const language = (visitor && visitor.language) || settings.get('Language') || 'en'; diff --git a/apps/meteor/app/livechat/server/methods/getCustomFields.js b/apps/meteor/app/livechat/server/methods/getCustomFields.js index c880d1cf509..a18c8018a64 100644 --- a/apps/meteor/app/livechat/server/methods/getCustomFields.js +++ b/apps/meteor/app/livechat/server/methods/getCustomFields.js @@ -1,8 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { LivechatCustomField } from '@rocket.chat/models'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + Meteor.methods({ async 'livechat:getCustomFields'() { + methodDeprecationLogger.warn('livechat:getCustomFields will be deprecated in future versions of Rocket.Chat'); return LivechatCustomField.find().toArray(); }, }); diff --git a/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.js b/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.js index 7e6e57f4c63..cda1b678dc3 100644 --- a/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.js +++ b/apps/meteor/app/livechat/server/methods/getDepartmentForwardRestrictions.js @@ -1,9 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:getDepartmentForwardRestrictions'(departmentId) { + methodDeprecationLogger.warn('livechat:getDepartmentForwardRestrictions will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:getDepartmentForwardRestrictions', diff --git a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js b/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js index 3c9cc060157..c32a883fd9f 100644 --- a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js +++ b/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js @@ -3,9 +3,11 @@ import { check } from 'meteor/check'; import { LivechatRooms, Messages } from '../../../models/server'; import { hasPermission } from '../../../authorization'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:getFirstRoomMessage'({ rid }) { + methodDeprecationLogger.warn('livechat:getFirstRoomMessage will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:getFirstRoomMessage', diff --git a/apps/meteor/app/livechat/server/methods/getNextAgent.js b/apps/meteor/app/livechat/server/methods/getNextAgent.js index d2f0074e2d1..f0a770bc79a 100644 --- a/apps/meteor/app/livechat/server/methods/getNextAgent.js +++ b/apps/meteor/app/livechat/server/methods/getNextAgent.js @@ -3,9 +3,11 @@ import { check } from 'meteor/check'; import { LivechatRooms, Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:getNextAgent'({ token, department }) { + methodDeprecationLogger.warn('livechat:getNextAgent will be deprecated in future versions of Rocket.Chat'); check(token, String); const room = LivechatRooms.findOpenByVisitorToken(token).fetch(); diff --git a/apps/meteor/app/livechat/server/methods/getTagsList.js b/apps/meteor/app/livechat/server/methods/getTagsList.js index 9510be4fc2b..f0329e83aef 100644 --- a/apps/meteor/app/livechat/server/methods/getTagsList.js +++ b/apps/meteor/app/livechat/server/methods/getTagsList.js @@ -1,9 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:getTagsList'() { + methodDeprecationLogger.warn('livechat:getTagsList will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:getTagsList', diff --git a/apps/meteor/app/livechat/server/methods/loadHistory.js b/apps/meteor/app/livechat/server/methods/loadHistory.js index 7e2fd97d532..eda067876ed 100644 --- a/apps/meteor/app/livechat/server/methods/loadHistory.js +++ b/apps/meteor/app/livechat/server/methods/loadHistory.js @@ -3,9 +3,12 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { loadMessageHistory } from '../../../lib'; import { LivechatRooms } from '../../../models/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) { + methodDeprecationLogger.warn('livechat:loadHistory will be deprecated in future versions of Rocket.Chat'); + if (!token || typeof token !== 'string') { return; } diff --git a/apps/meteor/app/livechat/server/methods/loginByToken.js b/apps/meteor/app/livechat/server/methods/loginByToken.js index 4c2c7c65836..07bca98f0cf 100644 --- a/apps/meteor/app/livechat/server/methods/loginByToken.js +++ b/apps/meteor/app/livechat/server/methods/loginByToken.js @@ -1,8 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { LivechatVisitors } from '@rocket.chat/models'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + Meteor.methods({ async 'livechat:loginByToken'(token) { + methodDeprecationLogger.warn('livechat:loginByToken will be deprecated in future versions of Rocket.Chat'); const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); if (!visitor) { diff --git a/apps/meteor/app/livechat/server/methods/pageVisited.js b/apps/meteor/app/livechat/server/methods/pageVisited.js index c9c88e95ddc..6c24b93a116 100644 --- a/apps/meteor/app/livechat/server/methods/pageVisited.js +++ b/apps/meteor/app/livechat/server/methods/pageVisited.js @@ -1,9 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:pageVisited'(token, room, pageInfo) { + methodDeprecationLogger.warn('livechat:pageVisited will be deprecated in future versions of Rocket.Chat'); Livechat.savePageHistory(token, room, pageInfo); }, }); diff --git a/apps/meteor/app/livechat/server/methods/registerGuest.js b/apps/meteor/app/livechat/server/methods/registerGuest.js index d242c365ef7..18d47caca6c 100644 --- a/apps/meteor/app/livechat/server/methods/registerGuest.js +++ b/apps/meteor/app/livechat/server/methods/registerGuest.js @@ -3,9 +3,12 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { Messages, LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) { + methodDeprecationLogger.warn('livechat:registerGuest will be deprecated in future versions of Rocket.Chat'); + const userId = await Livechat.registerGuest.call(this, { token, name, diff --git a/apps/meteor/app/livechat/server/methods/removeAgent.js b/apps/meteor/app/livechat/server/methods/removeAgent.js index 009b6badd3f..d9362b1eb1b 100644 --- a/apps/meteor/app/livechat/server/methods/removeAgent.js +++ b/apps/meteor/app/livechat/server/methods/removeAgent.js @@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:removeAgent'(username) { + methodDeprecationLogger.warn('livechat:removeAgent will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-agents')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeAgent', diff --git a/apps/meteor/app/livechat/server/methods/removeDepartment.js b/apps/meteor/app/livechat/server/methods/removeDepartment.js index 1d9d9323f75..226fb115337 100644 --- a/apps/meteor/app/livechat/server/methods/removeDepartment.js +++ b/apps/meteor/app/livechat/server/methods/removeDepartment.js @@ -2,9 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:removeDepartment'(_id) { + methodDeprecationLogger.warn('livechat:removeDepartment will be deprecated in future versions of Rocket.Chat'); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-departments')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeDepartment', diff --git a/apps/meteor/app/livechat/server/methods/removeManager.js b/apps/meteor/app/livechat/server/methods/removeManager.js index 7de6d8051a7..53eaa068ce6 100644 --- a/apps/meteor/app/livechat/server/methods/removeManager.js +++ b/apps/meteor/app/livechat/server/methods/removeManager.js @@ -2,9 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:removeManager'(username) { + methodDeprecationLogger.warn('livechat:removeManager will be deprecated in future versions of Rocket.Chat'); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-managers')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeManager', diff --git a/apps/meteor/app/livechat/server/methods/saveDepartmentAgents.js b/apps/meteor/app/livechat/server/methods/saveDepartmentAgents.js index 904bae1b8c4..4a26b45c63a 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartmentAgents.js +++ b/apps/meteor/app/livechat/server/methods/saveDepartmentAgents.js @@ -2,9 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:saveDepartmentAgents'(_id, departmentAgents) { + methodDeprecationLogger.warn('livechat:saveDepartmentAgents will be deprecated in future versions of Rocket.Chat'); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'add-livechat-department-agents')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveDepartmentAgents', diff --git a/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js b/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js index 7d908cdc7a9..0b5ac26021a 100644 --- a/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js +++ b/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js @@ -3,8 +3,12 @@ import { Match, check } from 'meteor/check'; import _ from 'underscore'; import { LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + Meteor.methods({ async 'livechat:saveSurveyFeedback'(visitorToken, visitorRoom, formData) { + methodDeprecationLogger.warn('livechat:saveSurveyFeedback will be deprecated in future versions of Rocket.Chat'); + check(visitorToken, String); check(visitorRoom, String); check(formData, [Match.ObjectIncluding({ name: String, value: String })]); diff --git a/apps/meteor/app/livechat/server/methods/searchAgent.js b/apps/meteor/app/livechat/server/methods/searchAgent.js index 600a28150ba..3d6751efa43 100644 --- a/apps/meteor/app/livechat/server/methods/searchAgent.js +++ b/apps/meteor/app/livechat/server/methods/searchAgent.js @@ -3,9 +3,12 @@ import _ from 'underscore'; import { hasPermission } from '../../../authorization'; import { Users } from '../../../models/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:searchAgent'(username) { + methodDeprecationLogger.warn('livechat:searchAgent will be deprecated in future versions of Rocket.Chat'); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:searchAgent', diff --git a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.js b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.js index 27e59f137df..00ddd26cba1 100644 --- a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.js +++ b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.js @@ -3,9 +3,12 @@ import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ 'livechat:sendOfflineMessage'(data) { + methodDeprecationLogger.warn('livechat:sendOfflineMessage will be deprecated in future versions of Rocket.Chat'); + check(data, { name: String, email: String, diff --git a/apps/meteor/app/livechat/server/methods/setCustomField.js b/apps/meteor/app/livechat/server/methods/setCustomField.js index e9764ed827a..67155cffb0f 100644 --- a/apps/meteor/app/livechat/server/methods/setCustomField.js +++ b/apps/meteor/app/livechat/server/methods/setCustomField.js @@ -2,9 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { LivechatVisitors, LivechatCustomField } from '@rocket.chat/models'; import { LivechatRooms } from '../../../models/server'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:setCustomField'(token, key, value, overwrite = true) { + methodDeprecationLogger.warn('livechat:setCustomField will be deprecated in future versions of Rocket.Chat'); + const customField = await LivechatCustomField.findOneById(key); if (customField) { if (customField.scope === 'room') { diff --git a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js index ada1703b34c..0fc5cab2cbc 100644 --- a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js +++ b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js @@ -5,9 +5,12 @@ import { LivechatVisitors } from '@rocket.chat/models'; import { LivechatRooms, Messages } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeTransferredByData } from '../lib/Helper'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ async 'livechat:setDepartmentForVisitor'({ roomId, visitorToken, departmentId } = {}) { + methodDeprecationLogger.warn('livechat:setDepartmentForVisitor will be deprecated in future versions of Rocket.Chat'); + check(roomId, String); check(visitorToken, String); check(departmentId, String); -- GitLab From 5474569f06db10ec76af3e0ae31b166443e6af35 Mon Sep 17 00:00:00 2001 From: Luciano Marcos Pierdona Junior <64279791+LucianoPierdona@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:57:22 -0300 Subject: [PATCH 024/107] [IMPROVE] Include `syncAvatars` on `ldap.syncNow` (#26824) --- apps/meteor/ee/server/api/ldap.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/ee/server/api/ldap.ts b/apps/meteor/ee/server/api/ldap.ts index d8919304696..acda37c9956 100644 --- a/apps/meteor/ee/server/api/ldap.ts +++ b/apps/meteor/ee/server/api/ldap.ts @@ -25,6 +25,7 @@ API.v1.addRoute( } await LDAPEE.sync(); + await LDAPEE.syncAvatars(); return API.v1.success({ message: 'Sync_in_progress' as const, -- GitLab From 29a6276333257d45ba609656349d3f3e0f6ca589 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Tue, 13 Sep 2022 02:11:29 +0530 Subject: [PATCH 025/107] [IMPROVE] Rounded video attachment (#26832) --- .../message/Attachments/Files/VideoAttachment.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx index a2b6327f9b7..d1c849f066d 100644 --- a/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx +++ b/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx @@ -1,5 +1,7 @@ import { VideoAttachmentProps } from '@rocket.chat/core-typings'; +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors'; import { useMediaUrl } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; @@ -13,6 +15,13 @@ import AttachmentSize from '../Attachment/AttachmentSize'; import AttachmentTitle from '../Attachment/AttachmentTitle'; import { useCollapse } from '../hooks/useCollapse'; +const videoAttachmentCss = css` + border: 2px solid ${colors.n200} !important; + border-radius: 2px; + display: flex; + flex-direction: column; +`; + export const VideoAttachment: FC<VideoAttachmentProps> = ({ title, video_url: url, @@ -35,7 +44,7 @@ export const VideoAttachment: FC<VideoAttachmentProps> = ({ {hasDownload && link && <AttachmentDownload title={title} href={getURL(link)} />} </AttachmentRow> {!collapsed && ( - <AttachmentContent width='full'> + <AttachmentContent width='full' className={videoAttachmentCss}> <Box is='video' width='full' controls preload='metadata'> <source src={getURL(url)} type={type} /> </Box> -- GitLab From ddd1e87cd142eb2bcbdcb7c1e8f237462ef5d477 Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:04:30 +0000 Subject: [PATCH 026/107] =?UTF-8?q?i18n:=20Language=20update=20from=20Ling?= =?UTF-8?q?oHub=20=F0=9F=A4=96=20on=202022-09-12Z=20(#26849)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index a2b9c2610c0..8d556ccb29a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5434,4 +5434,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Login Detected", "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", "Something_Went_Wrong": "Something went wrong" -} +} \ No newline at end of file -- GitLab From 3c2679baec46dd00dc4dec32f68af1415ce90f28 Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Tue, 13 Sep 2022 11:01:35 -0300 Subject: [PATCH 027/107] [FIX] Check if messsage.replies exist on new message template (#26652) --- .../MessageList/components/MessageContent.tsx | 4 +- .../room/MessageList/MessageContent.spec.tsx | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx index 1e4b10ff67b..94dbe18aa41 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx @@ -87,14 +87,14 @@ const MessageContent: FC<{ <ThreadMetric openThread={openThread(message._id)} counter={message.tcount} - following={Boolean(mineUid && message?.replies.indexOf(mineUid) > -1)} + following={Boolean(mineUid && message?.replies?.indexOf(mineUid) > -1)} mid={message._id} rid={message.rid} lm={message.tlm} unread={unread} mention={mention} all={all} - participants={message?.replies.length} + participants={message?.replies?.length} /> )} diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx b/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx new file mode 100644 index 00000000000..8715f92d16b --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@testing-library/react'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import React from 'react'; + +const date = new Date('2021-10-27T00:00:00.000Z'); +const baseMessage = { + ts: date, + u: { + _id: 'userId', + name: 'userName', + username: 'userName', + }, + msg: 'message', + rid: 'roomId', + _id: 'messageId', + _updatedAt: date, + urls: [], +}; +const MessageContent = proxyquire.noCallThru().load('../../../../../../client/views/room/MessageList/components/MessageContent', { + '../../../../lib/presence': { + UserPresence: () => '', + }, + '../../contexts/MessageContext': { + useMessageActions: () => ({ + actions: { + openRoom: () => '', + openThread: () => () => '', + replyBroadcast: () => '', + }, + }), + useMessageOembedIsEnabled: () => '', + useMessageRunActionLink: () => '', + }, + + '../../contexts/MessageListContext': { + useTranslateAttachments: () => '', + useMessageListShowReadReceipt: () => '', + }, + '../../../../hooks/useUserData': { + useUserData: () => '', + }, + '../../../blocks/MessageBlock': () => '', + './MessageContentBody': () => baseMessage.msg, +}).default; + +describe('MessageContent', () => { + it('should render the message when exists', () => { + render(<MessageContent message={baseMessage} sequential={false} id={''} />); + + expect(screen.getByText(baseMessage.msg)).to.exist; + }); + + it('should render the message when replies is undefined', () => { + render(<MessageContent message={{ ...baseMessage, replies: undefined, tcount: 0, tlm: date }} sequential={false} id={''} />); + + expect(screen.getByText(baseMessage.msg)).to.exist; + expect(screen.getByTitle('Replies')).to.include.text('0'); + }); +}); -- GitLab From fa9d85d5c20bd5388227958f91656c3ceac46c97 Mon Sep 17 00:00:00 2001 From: Jean Brito <jeanfbrito@gmail.com> Date: Tue, 13 Sep 2022 07:56:36 -0700 Subject: [PATCH 028/107] Chore: Add RocketChatDesktop function to open video calls when using Electron (#26793) Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> --- .../client/providers/VideoConfProvider.tsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/providers/VideoConfProvider.tsx b/apps/meteor/client/providers/VideoConfProvider.tsx index f6de91243f5..f29d2651e6b 100644 --- a/apps/meteor/client/providers/VideoConfProvider.tsx +++ b/apps/meteor/client/providers/VideoConfProvider.tsx @@ -8,6 +8,12 @@ import { VideoConfManager, DirectCallParams, ProviderCapabilities, CallPreferenc import VideoConfBlockModal from '../views/room/contextualBar/VideoConference/VideoConfBlockModal'; import VideoConfPopups from '../views/room/contextualBar/VideoConference/VideoConfPopups'; +type WindowMaybeDesktop = typeof window & { + RocketChatDesktop?: { + openInternalVideoChatWindow?: (url: string, options: undefined) => void; + }; +}; + const VideoConfContextProvider = ({ children }: { children: ReactNode }): ReactElement => { const [outgoing, setOutgoing] = useState<VideoConfPopupPayload | undefined>(); const setModal = useSetModal(); @@ -15,16 +21,21 @@ const VideoConfContextProvider = ({ children }: { children: ReactNode }): ReactE useEffect( () => VideoConfManager.on('call/join', (props) => { - const open = (): void => { - const popup = window.open(props.url); + const windowMaybeDesktop = window as WindowMaybeDesktop; + if (windowMaybeDesktop.RocketChatDesktop?.openInternalVideoChatWindow) { + windowMaybeDesktop.RocketChatDesktop.openInternalVideoChatWindow(props.url, undefined); + } else { + const open = (): void => { + const popup = window.open(props.url); - if (popup !== null) { - return; - } + if (popup !== null) { + return; + } - setModal(<VideoConfBlockModal onClose={(): void => setModal(null)} onConfirm={open} />); - }; - open(); + setModal(<VideoConfBlockModal onClose={(): void => setModal(null)} onConfirm={open} />); + }; + open(); + } }), [setModal], ); -- GitLab From f7438d335f59ebdf55e7417f0868357152aef5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <henrique.jobs1@gmail.com> Date: Tue, 13 Sep 2022 16:59:13 -0300 Subject: [PATCH 029/107] Chore: Rewrite apps WarningModal component to typescript (#26845) --- .../apps/{WarningModal.js => WarningModal.tsx} | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename apps/meteor/client/views/admin/apps/{WarningModal.js => WarningModal.tsx} (74%) diff --git a/apps/meteor/client/views/admin/apps/WarningModal.js b/apps/meteor/client/views/admin/apps/WarningModal.tsx similarity index 74% rename from apps/meteor/client/views/admin/apps/WarningModal.js rename to apps/meteor/client/views/admin/apps/WarningModal.tsx index 8fc6a80bb6e..11ba65d3ae3 100644 --- a/apps/meteor/client/views/admin/apps/WarningModal.js +++ b/apps/meteor/client/views/admin/apps/WarningModal.tsx @@ -1,8 +1,17 @@ import { Button, Modal } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; -const WarningModal = ({ text, confirmText, close, cancel, cancelText, confirm, ...props }) => { +type WarningModalProps = { + text: string; + confirmText: string; + close: () => void; + cancel: () => void; + cancelText: string; + confirm: () => Promise<void>; +}; + +const WarningModal = ({ text, confirmText, close, cancel, cancelText, confirm, ...props }: WarningModalProps): ReactElement => { const t = useTranslation(); return ( <Modal {...props}> -- GitLab From ddd52cb279bb2032c101cf1f8bb4ea088a163158 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 13 Sep 2022 18:15:09 -0300 Subject: [PATCH 030/107] [FIX] Importer fails when file includes user without an email. (#26836) --- packages/rest-typings/src/v1/import/StartImportParamsPOST.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rest-typings/src/v1/import/StartImportParamsPOST.ts b/packages/rest-typings/src/v1/import/StartImportParamsPOST.ts index 6ee2fb12036..6f34cddf593 100644 --- a/packages/rest-typings/src/v1/import/StartImportParamsPOST.ts +++ b/packages/rest-typings/src/v1/import/StartImportParamsPOST.ts @@ -44,13 +44,13 @@ const StartImportParamsPostSchema = { properties: { user_id: { type: 'string' }, // eslint-disable-line username: { type: 'string' }, - email: { type: 'string' }, + email: { type: 'string', nullable: true }, is_deleted: { type: 'boolean' }, // eslint-disable-line is_bot: { type: 'boolean' }, // eslint-disable-line do_import: { type: 'boolean' }, // eslint-disable-line is_email_taken: { type: 'boolean' }, // eslint-disable-line }, - required: ['user_id', 'username', 'email', 'is_deleted', 'is_bot', 'do_import', 'is_email_taken'], + required: ['user_id', 'username', 'is_deleted', 'is_bot', 'do_import', 'is_email_taken'], }, }, channels: { -- GitLab From 2bb9f22feba2663958c621bcbc385daace361b09 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:48:59 -0300 Subject: [PATCH 031/107] Chore: Update fuselage to next version. (#26841) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- yarn.lock | 80 +++++++++++++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/yarn.lock b/yarn.lock index d85300fb7f8..922aca62b36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4796,16 +4796,16 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:~0.31.19-dev.10": - version: 0.31.19-dev.10 - resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.10" +"@rocket.chat/css-in-js@npm:~0.31.19-dev.16": + version: 0.31.19-dev.16 + resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.16" dependencies: "@emotion/hash": ^0.8.0 - "@rocket.chat/css-supports": ~0.31.19-dev.10 - "@rocket.chat/memo": ~0.31.19-dev.10 - "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.10 + "@rocket.chat/css-supports": ~0.31.19-dev.16 + "@rocket.chat/memo": ~0.31.19-dev.16 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.16 stylis: ~4.0.13 - checksum: 9e69aae1bb1243abbc4a9fd864817e38d8ace88e80f39f9c58701afcf589cb8080d308943c431bea8ff48854dec756637bc8bb4d0e5cdad621406050cb86fd46 + checksum: 89b930290107a51e0a4002447ea777eb816faf41a78209f9467a96dbbda1addee29892fbc63c0fde29101c35ba1b293b0d86eb144e9c1ce9c91df4351a4229f0 languageName: node linkType: hard @@ -4818,12 +4818,12 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-supports@npm:~0.31.19-dev.10": - version: 0.31.19-dev.10 - resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.10" +"@rocket.chat/css-supports@npm:~0.31.19-dev.16": + version: 0.31.19-dev.16 + resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.16" dependencies: - "@rocket.chat/memo": ~0.31.19-dev.10 - checksum: 4374ebc9bf43d3b6ab42d1f2daa5d426950d32a49de19b97e1f54214e1b608d21b9311883419138774bb41d220e5e23ab5195d1b1ae40b6d03d931502c488b15 + "@rocket.chat/memo": ~0.31.19-dev.16 + checksum: 665401150834508f5f13520a0639d09d3c79ea9914f1a23f2b2cb859d4cdc9842370438f4acc1023468f603b34a126654f1e9ea5668c1736f29b8b06bd3a59f8 languageName: node linkType: hard @@ -5019,10 +5019,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.92": - version: 0.32.0-dev.92 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.92" - checksum: 57121863319fed1a69f05f95eb28478bd8e8a3fd0b99712adf61a22f0f88b874f65242b53bb5353c06a949def0e0f737fdba377f1053ff6662db9887fd9658cd +"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.98": + version: 0.32.0-dev.98 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.98" + checksum: 715dc2d4e5ec4e6ca2bbfdc6eb8844c0eb7135ad020b60f4c2f80c864dd8b2a9823b2ac8328dfdcf380bdeec67a05595d159eb0fd4e09fd80b81107ca26462b6 languageName: node linkType: hard @@ -5091,14 +5091,14 @@ __metadata: linkType: soft "@rocket.chat/fuselage@npm:next": - version: 0.32.0-dev.142 - resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.142" - dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.10 - "@rocket.chat/css-supports": ~0.31.19-dev.10 - "@rocket.chat/fuselage-tokens": ~0.32.0-dev.92 - "@rocket.chat/memo": ~0.31.19-dev.10 - "@rocket.chat/styled": ~0.31.19-dev.10 + version: 0.32.0-dev.148 + resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.148" + dependencies: + "@rocket.chat/css-in-js": ~0.31.19-dev.16 + "@rocket.chat/css-supports": ~0.31.19-dev.16 + "@rocket.chat/fuselage-tokens": ~0.32.0-dev.98 + "@rocket.chat/memo": ~0.31.19-dev.16 + "@rocket.chat/styled": ~0.31.19-dev.16 invariant: ^2.2.4 react-keyed-flatten-children: ^1.3.0 peerDependencies: @@ -5108,7 +5108,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: fd9f30f2229c8e41b46e23b37beb5acc58c796a06cfc0fd1f7c1c877b349db257af58b31a12ab4a5606b6a6d8ae268ad2424893226d93cb373bcb8207bfbb656 + checksum: e8128de26fc98c57669ad56dbff67c34759c247c299b16c49188bf82260e65f96a90859051566ef52e301735657c9d564b02200ff5ff0b9e0437aae876564111 languageName: node linkType: hard @@ -5296,10 +5296,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/memo@npm:~0.31.19-dev.10": - version: 0.31.19-dev.10 - resolution: "@rocket.chat/memo@npm:0.31.19-dev.10" - checksum: de95f407d449c04bd61a4a1c9aea81ba5ee0bb421c168b9297d49de60fcb04b6ae6aa0e3f8ebca03ba38b815238c0fe367541a171943bda0521598ee7c429d64 +"@rocket.chat/memo@npm:~0.31.19-dev.16": + version: 0.31.19-dev.16 + resolution: "@rocket.chat/memo@npm:0.31.19-dev.16" + checksum: 3b8c587175e88c06dad70defca1727972b503f6dbee4992630c92580ea2f74abfe14bda91608ab690a2bdb4da42ab93f9770f1818a26c9bcd2eb47ccacf35f53 languageName: node linkType: hard @@ -5766,13 +5766,13 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/styled@npm:~0.31.19-dev.10": - version: 0.31.19-dev.10 - resolution: "@rocket.chat/styled@npm:0.31.19-dev.10" +"@rocket.chat/styled@npm:~0.31.19-dev.16": + version: 0.31.19-dev.16 + resolution: "@rocket.chat/styled@npm:0.31.19-dev.16" dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.10 + "@rocket.chat/css-in-js": ~0.31.19-dev.16 tslib: ^2.3.1 - checksum: 0ebfbdbd374e2ef861bae34344160bbdf0a9aafe6a29b455202326c25daaf36e3b934b773938ca03e2e5b95166e5996fb23b93a1ff033034e8f1fd59e2791b89 + checksum: 685654d2f8c61738ef0557179e9cc2ee9501c762bb8ead5ebb297f5c44756a5e055de5069a9ae945e328ca60a6eb69cede0697adf72fd6aea00134af19f110fb languageName: node linkType: hard @@ -5788,15 +5788,15 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.10": - version: 0.31.19-dev.10 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.10" +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.16": + version: 0.31.19-dev.16 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.16" dependencies: - "@rocket.chat/css-supports": ~0.31.19-dev.10 + "@rocket.chat/css-supports": ~0.31.19-dev.16 tslib: ^2.3.1 peerDependencies: stylis: 4.0.10 - checksum: d2a2baac81ae7bd1dfe62dbbc14350eda95c1419f211648f1ff4148d3700a4270cb5c4922dd79b3276257af09efcd42d95c6b898653b2e67fbae1f1b64ff245a + checksum: 5de7d4d994e3d9ceb48eff8e2566276268a20e78cacc12f9298846adf637b97c728b9b78fb23dfc446783db71a311af9cfb2e8bfd63792754f37bef233e438b3 languageName: node linkType: hard @@ -21842,7 +21842,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 + checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d languageName: node linkType: hard -- GitLab From bb3756433fcab86753b1809d28694feca3d78cd5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Wed, 14 Sep 2022 14:21:00 -0300 Subject: [PATCH 032/107] Chore: fix wrong `test.step` usage (#26873) --- .../meteor/tests/e2e/omnichannel-currentChats.spec.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts b/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts index 6a0e26adea5..e703cdbf089 100644 --- a/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-currentChats.spec.ts @@ -12,7 +12,7 @@ test.describe.serial('Current Chats', () => { await page.goto('/omnichannel/current'); }); - test.describe('Render Fields', async () => { + test('Render Fields', async () => { await test.step('expect show guest Field', async () => { await expect(pageOmnichannelCurrentChats.guestField).toBeVisible(); }); @@ -42,12 +42,9 @@ test.describe.serial('Current Chats', () => { }); }); - test.describe('Render Options', async () => { - test.beforeEach(async () => { - await pageOmnichannelCurrentChats.doOpenOptions(); - }); - + test('Render Options', async () => { await test.step('expect show form options button', async () => { + await pageOmnichannelCurrentChats.doOpenOptions(); await expect(pageOmnichannelCurrentChats.formOptions).toBeVisible(); }); @@ -64,7 +61,7 @@ test.describe.serial('Current Chats', () => { }); }); - test.describe('Render Table', async () => { + test('Render Table', async () => { await test.step('expect show table', async () => { await expect(pageOmnichannelCurrentChats.table).toBeVisible(); }); -- GitLab From e83f80ecb15b179f429d34dd4d7d0b2cb68fc03e Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Wed, 14 Sep 2022 15:59:35 -0300 Subject: [PATCH 033/107] Chore: Upgrading livechat's ui-kit package to latest version (#26709) --- packages/livechat/package.json | 2 +- .../uiKit/message/ActionsBlock/index.js | 5 ++-- .../uiKit/message/ButtonElement/index.js | 6 ++--- .../uiKit/message/ButtonElement/stories.js | 4 +-- .../uiKit/message/ContextBlock/index.js | 4 +-- .../uiKit/message/ImageElement/index.js | 6 ++--- .../uiKit/message/SectionBlock/index.js | 8 +++--- .../src/components/uiKit/message/index.js | 26 +++++++++---------- yarn.lock | 10 +++---- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 8fbe54e8bcd..0c990c0db17 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -87,7 +87,7 @@ }, "dependencies": { "@rocket.chat/sdk": "^1.0.0-alpha.42", - "@rocket.chat/ui-kit": "^0.14.1", + "@rocket.chat/ui-kit": "^0.31.16", "crypto-js": "^4.1.1", "css-vars-ponyfill": "^2.4.7", "date-fns": "^2.15.0", diff --git a/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js b/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js index c8c7fe66f5a..1fcaceed526 100644 --- a/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/ActionsBlock/index.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { useState, useMemo, useCallback } from 'preact/compat'; import { withTranslation } from 'react-i18next'; @@ -21,7 +21,8 @@ const ActionsBlock = ({ appId, blockId, elements, parser, t }) => { <Block appId={appId} blockId={blockId}> <div className={createClassName(styles, 'uikit-actions-block')}> {renderableElements.map((element, key) => { - const renderedElement = parser.renderActions(element, BLOCK_CONTEXT.ACTION); + const renderedElement = parser.renderActions(element, BlockContext.ACTION); + if (!renderedElement) { return null; } diff --git a/packages/livechat/src/components/uiKit/message/ButtonElement/index.js b/packages/livechat/src/components/uiKit/message/ButtonElement/index.js index 663ffac506f..46e28291ec4 100644 --- a/packages/livechat/src/components/uiKit/message/ButtonElement/index.js +++ b/packages/livechat/src/components/uiKit/message/ButtonElement/index.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { memo, useCallback } from 'preact/compat'; import { createClassName } from '../../../helpers'; @@ -35,8 +35,8 @@ const ButtonElement = ({ text, actionId, url, value, style, context, confirm, pa children={parser.text(text)} className={createClassName(styles, 'uikit-button', { style, - accessory: context === BLOCK_CONTEXT.SECTION, - action: context === BLOCK_CONTEXT.ACTION, + accessory: context === BlockContext.SECTION, + action: context === BlockContext.ACTION, })} disabled={performingAction} type='button' diff --git a/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js b/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js index 9655a46db59..1f75aaae0cc 100644 --- a/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js +++ b/packages/livechat/src/components/uiKit/message/ButtonElement/stories.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { action } from '@storybook/addon-actions'; import ButtonElement from '.'; @@ -27,7 +27,7 @@ export default { url: { control: 'text' }, value: { control: 'text' }, style: { control: { type: 'inline-radio', options: [null, 'primary', 'danger'] } }, - context: { control: { type: 'select', options: BLOCK_CONTEXT } }, + context: { control: { type: 'select', options: BlockContext } }, }, args: { text: { type: 'plain_text', text: 'Click Me' }, diff --git a/packages/livechat/src/components/uiKit/message/ContextBlock/index.js b/packages/livechat/src/components/uiKit/message/ContextBlock/index.js index d3546d3061c..4d9be48bdfb 100644 --- a/packages/livechat/src/components/uiKit/message/ContextBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/ContextBlock/index.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { memo } from 'preact/compat'; import { createClassName } from '../../../helpers'; @@ -10,7 +10,7 @@ const ContextBlock = ({ appId, blockId, elements, parser }) => ( <div className={createClassName(styles, 'uikit-context-block')}> {elements.map((element, key) => ( <div key={key} className={createClassName(styles, 'uikit-context-block__item')}> - {parser.renderContext(element, BLOCK_CONTEXT.CONTEXT)} + {parser.renderContext(element, BlockContext.CONTEXT)} </div> ))} </div> diff --git a/packages/livechat/src/components/uiKit/message/ImageElement/index.js b/packages/livechat/src/components/uiKit/message/ImageElement/index.js index 198b773a621..495b9895ddd 100644 --- a/packages/livechat/src/components/uiKit/message/ImageElement/index.js +++ b/packages/livechat/src/components/uiKit/message/ImageElement/index.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { memo } from 'preact/compat'; import { createClassName } from '../../../helpers'; @@ -8,8 +8,8 @@ const ImageElement = ({ imageUrl, altText, context }) => ( <div aria-label={altText} className={createClassName(styles, 'uikit-image', { - accessory: context === BLOCK_CONTEXT.SECTION, - context: context === BLOCK_CONTEXT.CONTEXT, + accessory: context === BlockContext.SECTION, + context: context === BlockContext.CONTEXT, })} role='img' style={{ diff --git a/packages/livechat/src/components/uiKit/message/SectionBlock/index.js b/packages/livechat/src/components/uiKit/message/SectionBlock/index.js index 01c5f66d4c9..c09d0e95b0a 100644 --- a/packages/livechat/src/components/uiKit/message/SectionBlock/index.js +++ b/packages/livechat/src/components/uiKit/message/SectionBlock/index.js @@ -1,4 +1,4 @@ -import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; import { memo } from 'preact/compat'; import { createClassName } from '../../../helpers'; @@ -9,12 +9,12 @@ const SectionBlock = ({ appId, blockId, text, fields, accessory, parser }) => ( <Block appId={appId} blockId={blockId}> <div className={createClassName(styles, 'uikit-section-block')}> <div className={createClassName(styles, 'uikit-section-block__content')}> - {text && <div className={createClassName(styles, 'uikit-section-block__text')}>{parser.text(text, BLOCK_CONTEXT.SECTION)}</div>} + {text && <div className={createClassName(styles, 'uikit-section-block__text')}>{parser.text(text, BlockContext.SECTION)}</div>} {Array.isArray(fields) && fields.length > 0 && ( <div className={createClassName(styles, 'uikit-section-block__fields')}> {fields.map((field, i) => ( <div key={i} className={createClassName(styles, 'uikit-section-block__field')}> - {parser.text(field, BLOCK_CONTEXT.SECTION)} + {parser.text(field, BlockContext.SECTION)} </div> ))} </div> @@ -22,7 +22,7 @@ const SectionBlock = ({ appId, blockId, text, fields, accessory, parser }) => ( </div> {accessory && ( <div className={createClassName(styles, 'uikit-section-block__accessory')}> - {parser.renderAccessories(accessory, BLOCK_CONTEXT.SECTION)} + {parser.renderAccessories(accessory, BlockContext.SECTION)} </div> )} </div> diff --git a/packages/livechat/src/components/uiKit/message/index.js b/packages/livechat/src/components/uiKit/message/index.js index 1424e89d581..e7caf59d9de 100644 --- a/packages/livechat/src/components/uiKit/message/index.js +++ b/packages/livechat/src/components/uiKit/message/index.js @@ -1,4 +1,4 @@ -import { uiKitMessage, UiKitParserMessage, BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; +import { uiKitMessage, UiKitParserMessage, BlockContext } from '@rocket.chat/ui-kit'; import ActionsBlock from './ActionsBlock'; import ButtonElement from './ButtonElement'; @@ -15,7 +15,7 @@ import StaticSelectElement from './StaticSelectElement'; class MessageParser extends UiKitParserMessage { divider = (element, context, index) => { - if (context !== BLOCK_CONTEXT.BLOCK) { + if (context !== BlockContext.BLOCK) { return null; } @@ -23,7 +23,7 @@ class MessageParser extends UiKitParserMessage { }; section = (element, context, index) => { - if (context !== BLOCK_CONTEXT.BLOCK) { + if (context !== BlockContext.BLOCK) { return null; } @@ -31,7 +31,7 @@ class MessageParser extends UiKitParserMessage { }; image = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return <ImageBlock key={index} {...element} parser={this} />; } @@ -39,7 +39,7 @@ class MessageParser extends UiKitParserMessage { }; actions = (element, context, index) => { - if (context !== BLOCK_CONTEXT.BLOCK) { + if (context !== BlockContext.BLOCK) { return null; } @@ -47,15 +47,15 @@ class MessageParser extends UiKitParserMessage { }; context = (element, context, index) => { - if (context !== BLOCK_CONTEXT.BLOCK) { + if (context !== BlockContext.BLOCK) { return null; } return <ContextBlock key={index} {...element} parser={this} />; }; - plainText = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + plain_text = (element, context, index) => { + if (context === BlockContext.BLOCK) { return null; } @@ -63,7 +63,7 @@ class MessageParser extends UiKitParserMessage { }; mrkdwn = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return null; } @@ -71,7 +71,7 @@ class MessageParser extends UiKitParserMessage { }; button = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return null; } @@ -79,7 +79,7 @@ class MessageParser extends UiKitParserMessage { }; overflow = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return null; } @@ -87,7 +87,7 @@ class MessageParser extends UiKitParserMessage { }; datePicker = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return null; } @@ -95,7 +95,7 @@ class MessageParser extends UiKitParserMessage { }; staticSelect = (element, context, index) => { - if (context === BLOCK_CONTEXT.BLOCK) { + if (context === BlockContext.BLOCK) { return null; } diff --git a/yarn.lock b/yarn.lock index 922aca62b36..3d96d2eacc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5202,7 +5202,7 @@ __metadata: "@rocket.chat/fuselage-tokens": 0.31.18 "@rocket.chat/logo": 0.31.18 "@rocket.chat/sdk": ^1.0.0-alpha.42 - "@rocket.chat/ui-kit": ^0.14.1 + "@rocket.chat/ui-kit": ^0.31.16 "@storybook/addon-actions": ~6.5.10 "@storybook/addon-backgrounds": ~6.5.10 "@storybook/addon-essentials": ~6.5.10 @@ -5882,10 +5882,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/ui-kit@npm:^0.14.1": - version: 0.14.1 - resolution: "@rocket.chat/ui-kit@npm:0.14.1" - checksum: c9da95fbb079b2bbd39d746552083f0a67171347f303a3f27cad875f0eedf8d121d372fd0671c2ba9b2cd93b727fec10b01fe906cd71057a98872338c58f48bb +"@rocket.chat/ui-kit@npm:^0.31.16": + version: 0.31.16 + resolution: "@rocket.chat/ui-kit@npm:0.31.16" + checksum: 08e161a629476338aacbe9a9b46feeb372f052c4548999b2611894cf9778b1dab4ac48586c43827345d8024c86ddea509022d627bbb4bb39ca1b667172260a38 languageName: node linkType: hard -- GitLab From 0d541859b90f1c6c25ca4792dc8159a8b6f28119 Mon Sep 17 00:00:00 2001 From: Hugo Costa <hugocarreiracosta@gmail.com> Date: Wed, 14 Sep 2022 18:11:54 -0300 Subject: [PATCH 034/107] [FIX] Expanded thread behind sidebar on small screens (#26852) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .../client/views/room/threads/ThreadView.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/threads/ThreadView.tsx b/apps/meteor/client/views/room/threads/ThreadView.tsx index 0f8a50bd36d..1e5be587ea4 100644 --- a/apps/meteor/client/views/room/threads/ThreadView.tsx +++ b/apps/meteor/client/views/room/threads/ThreadView.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { Modal, Box } from '@rocket.chat/fuselage'; import { useLayoutContextualBarExpanded, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ComponentProps, useCallback, useMemo, forwardRef } from 'react'; @@ -50,18 +51,28 @@ const ThreadView = forwardRef<HTMLElement, ThreadViewProps>(function ThreadView( onToggleFollow(following); }, [following, onToggleFollow]); + const expandedThreadStyle = + hasExpand && expanded + ? css` + max-width: 855px !important; + @media (min-width: 780px) and (max-width: 1135px) { + max-width: calc(100% - var(--sidebar-width)) !important; + } + ` + : undefined; + return ( <> {hasExpand && expanded && <Modal.Backdrop onClick={onClose} />} <Box flexGrow={1} position={expanded ? 'static' : 'relative'}> <VerticalBar - className='rcx-thread-view' + rcx-thread-view + className={expandedThreadStyle} position={hasExpand && expanded ? 'fixed' : 'absolute'} display='flex' flexDirection='column' width={'full'} - maxWidth={hasExpand && expanded ? 855 : undefined} overflow='hidden' zIndex={100} insetBlock={0} -- GitLab From f4b56815c1caff5febce85f479319aa0be39d6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <henrique.jobs1@gmail.com> Date: Thu, 15 Sep 2022 01:15:39 -0300 Subject: [PATCH 035/107] Chore: Bump fuselage dependencies and implement new tabs variant in marketplace (#26876) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/ee/server/services/package.json | 10 +- apps/meteor/package.json | 28 +- ee/apps/ddp-streamer/package.json | 4 +- packages/core-typings/package.json | 6 +- packages/fuselage-ui-kit/package.json | 10 +- packages/gazzodown/package.json | 8 +- packages/livechat/package.json | 6 +- packages/rest-typings/package.json | 4 +- packages/ui-client/package.json | 6 +- packages/ui-contexts/package.json | 4 +- packages/ui-video-conf/package.json | 6 +- yarn.lock | 337 ++++++++------------ 12 files changed, 182 insertions(+), 247 deletions(-) diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index dac31faf170..f22ecc5cc62 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -24,13 +24,13 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "0.31.18", - "@rocket.chat/message-parser": "0.31.18", + "@rocket.chat/emitter": "next", + "@rocket.chat/message-parser": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "0.31.18", - "@rocket.chat/ui-kit": "0.31.18", + "@rocket.chat/string-helpers": "next", + "@rocket.chat/ui-kit": "next", "ajv": "^8.11.0", "bcrypt": "^5.0.1", "body-parser": "^1.20.0", @@ -54,7 +54,7 @@ "ws": "^8.8.1" }, "devDependencies": { - "@rocket.chat/icons": "0.31.18", + "@rocket.chat/icons": "next", "@types/cookie": "^0.5.1", "@types/cookie-parser": "^1.4.3", "@types/ejson": "^2.2.0", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 6426511a827..cd1588a5e36 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -201,33 +201,33 @@ "@rocket.chat/apps-engine": "1.34.0", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/css-in-js": "0.31.18", - "@rocket.chat/emitter": "0.31.18", + "@rocket.chat/css-in-js": "next", + "@rocket.chat/emitter": "next", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "0.31.18", - "@rocket.chat/fuselage-polyfills": "0.31.18", - "@rocket.chat/fuselage-toastbar": "0.31.18", - "@rocket.chat/fuselage-tokens": "0.31.18", - "@rocket.chat/fuselage-ui-kit": "0.31.18", + "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/fuselage-polyfills": "next", + "@rocket.chat/fuselage-toastbar": "next", + "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage-ui-kit": "next", "@rocket.chat/gazzodown": "workspace:^", - "@rocket.chat/icons": "0.31.18", + "@rocket.chat/icons": "next", "@rocket.chat/layout": "next", - "@rocket.chat/logo": "0.31.18", - "@rocket.chat/memo": "0.31.18", - "@rocket.chat/message-parser": "0.31.18", + "@rocket.chat/logo": "next", + "@rocket.chat/memo": "next", + "@rocket.chat/message-parser": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", - "@rocket.chat/onboarding-ui": "0.31.18", + "@rocket.chat/onboarding-ui": "next", "@rocket.chat/poplib": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "0.31.18", + "@rocket.chat/string-helpers": "next", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@rocket.chat/ui-kit": "0.31.18", + "@rocket.chat/ui-kit": "next", "@rocket.chat/ui-video-conf": "workspace:^", "@slack/rtm-api": "^6.0.0", "@tanstack/react-query": "^4.0.10", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 01639c2924f..f4f7d764cad 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -16,11 +16,11 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "0.31.18", + "@rocket.chat/emitter": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "0.31.18", + "@rocket.chat/string-helpers": "next", "@rocket.chat/ui-contexts": "workspace:^", "colorette": "^1.4.0", "ejson": "^2.2.2", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 3a181f6eb1b..7faf5cf4e29 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -23,8 +23,8 @@ ], "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", - "@rocket.chat/icons": "0.31.18", - "@rocket.chat/message-parser": "0.31.18", - "@rocket.chat/ui-kit": "0.31.18" + "@rocket.chat/icons": "next", + "@rocket.chat/message-parser": "next", + "@rocket.chat/ui-kit": "next" } } diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index faf9c4db011..b89ffc23b27 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -50,11 +50,11 @@ "@rocket.chat/apps-engine": "~1.30.0", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "0.31.18", - "@rocket.chat/fuselage-polyfills": "0.31.18", - "@rocket.chat/icons": "0.31.18", + "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/fuselage-polyfills": "next", + "@rocket.chat/icons": "next", "@rocket.chat/prettier-config": "next", - "@rocket.chat/styled": "0.31.18", + "@rocket.chat/styled": "next", "@storybook/addon-essentials": "~6.5.10", "@storybook/addons": "~6.5.10", "@storybook/builder-webpack5": "~6.5.10", @@ -78,7 +78,7 @@ "webpack": "~5.68.0" }, "dependencies": { - "@rocket.chat/ui-kit": "0.31.18", + "@rocket.chat/ui-kit": "next", "tslib": "^2.3.1" } } diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index c5afba1ef46..b1e9ce93dfb 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,11 +6,11 @@ "@babel/core": "^7.18.9", "@mdx-js/react": "^1.6.22", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/css-in-js": "0.31.18", + "@rocket.chat/css-in-js": "next", "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-tokens": "0.31.18", - "@rocket.chat/message-parser": "0.31.18", - "@rocket.chat/styled": "0.31.18", + "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/message-parser": "next", + "@rocket.chat/styled": "next", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", "@storybook/addon-actions": "~6.5.10", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 0c990c0db17..d87419d4658 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -26,8 +26,8 @@ "@babel/eslint-parser": "^7.18.9", "@babel/preset-env": "^7.18.9", "@rocket.chat/eslint-config": "^0.4.0", - "@rocket.chat/fuselage-tokens": "0.31.18", - "@rocket.chat/logo": "0.31.18", + "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/logo": "next", "@storybook/addon-actions": "~6.5.10", "@storybook/addon-backgrounds": "~6.5.10", "@storybook/addon-essentials": "~6.5.10", @@ -87,7 +87,7 @@ }, "dependencies": { "@rocket.chat/sdk": "^1.0.0-alpha.42", - "@rocket.chat/ui-kit": "^0.31.16", + "@rocket.chat/ui-kit": "next", "crypto-js": "^4.1.1", "css-vars-ponyfill": "^2.4.7", "date-fns": "^2.15.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 0899a3fcffe..0b8648c0d62 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -26,8 +26,8 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/message-parser": "0.31.18", - "@rocket.chat/ui-kit": "0.31.18", + "@rocket.chat/message-parser": "next", + "@rocket.chat/ui-kit": "next", "ajv": "^8.11.0" } } diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index bd9d270b3d8..ce17f2da0d9 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -3,10 +3,10 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@rocket.chat/css-in-js": "0.31.18", + "@rocket.chat/css-in-js": "next", "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "0.31.18", - "@rocket.chat/icons": "0.31.18", + "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/icons": "next", "@rocket.chat/ui-contexts": "workspace:~", "@storybook/addon-actions": "~6.5.10", "@storybook/addon-docs": "~6.5.10", diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 43295e6275c..7858794225d 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -4,8 +4,8 @@ "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "0.31.18", - "@rocket.chat/fuselage-hooks": "0.31.18", + "@rocket.chat/emitter": "next", + "@rocket.chat/fuselage-hooks": "next", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "^27.4.1", "@types/react": "^17.0.47", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index e96b6c1f048..7bab37906fa 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -3,11 +3,11 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@rocket.chat/css-in-js": "0.31.18", + "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "0.31.18", - "@rocket.chat/styled": "0.31.18", + "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/styled": "next", "@types/jest": "^27.4.1", "eslint": "^8.20.0", "eslint-plugin-react": "^7.30.1", diff --git a/yarn.lock b/yarn.lock index 3d96d2eacc2..f6b742011b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4773,9 +4773,9 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": 0.31.18 - "@rocket.chat/message-parser": 0.31.18 - "@rocket.chat/ui-kit": 0.31.18 + "@rocket.chat/icons": next + "@rocket.chat/message-parser": next + "@rocket.chat/ui-kit": next eslint: ^8.20.0 mongodb: ^4.3.1 prettier: ^2.7.1 @@ -4783,47 +4783,25 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/css-in-js@npm:0.31.18, @rocket.chat/css-in-js@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/css-in-js@npm:0.31.18" +"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.19-dev.19": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.19" dependencies: "@emotion/hash": ^0.8.0 - "@rocket.chat/css-supports": ^0.31.18 - "@rocket.chat/memo": ^0.31.18 - "@rocket.chat/stylis-logical-props-middleware": ^0.31.18 + "@rocket.chat/css-supports": ~0.31.19-dev.19 + "@rocket.chat/memo": ~0.31.19-dev.19 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.19 stylis: ~4.0.13 - checksum: 4862ec053fe14a3a185c3374d265ff28c72208496aadf7a66e9abf8a3aa2cc6e3c00e3b08821e2ccbdce210b15d9565eec1f3014bb449dd6fb0ea35628fcd14a + checksum: 1f0a1de2da1f7a50526b5f3a5bb33dc9cdaf53f450d6c6403c3460e15f308bd6e2eeb5e4ca219f909b99b8f94a1b1ded6c45578b424d3a4f5029820ed4880d1f languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:~0.31.19-dev.16": - version: 0.31.19-dev.16 - resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.16" +"@rocket.chat/css-supports@npm:~0.31.19-dev.19": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.19" dependencies: - "@emotion/hash": ^0.8.0 - "@rocket.chat/css-supports": ~0.31.19-dev.16 - "@rocket.chat/memo": ~0.31.19-dev.16 - "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.16 - stylis: ~4.0.13 - checksum: 89b930290107a51e0a4002447ea777eb816faf41a78209f9467a96dbbda1addee29892fbc63c0fde29101c35ba1b293b0d86eb144e9c1ce9c91df4351a4229f0 - languageName: node - linkType: hard - -"@rocket.chat/css-supports@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/css-supports@npm:0.31.18" - dependencies: - "@rocket.chat/memo": ^0.31.18 - checksum: a905044523f4f9caa2b3cc8a7b883b7e9320b64e452bb02bd97ccb6fe857c46750cd9913b3bd4cda5e4b6563c9d8c1e52030a9fcc1c5a09468c8578c8ddee6be - languageName: node - linkType: hard - -"@rocket.chat/css-supports@npm:~0.31.19-dev.16": - version: 0.31.19-dev.16 - resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.16" - dependencies: - "@rocket.chat/memo": ~0.31.19-dev.16 - checksum: 665401150834508f5f13520a0639d09d3c79ea9914f1a23f2b2cb859d4cdc9842370438f4acc1023468f603b34a126654f1e9ea5668c1736f29b8b06bd3a59f8 + "@rocket.chat/memo": ~0.31.19-dev.19 + checksum: ea8aaab14336894d028bb9ef86db2efa7df9252c403b6faac50276708431a2ffe29150acacf836f1e4aa05a2ac87c42a8e5821c0903bd42158e4c9adfc71e226 languageName: node linkType: hard @@ -4833,12 +4811,12 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": 0.31.18 + "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": 0.31.18 + "@rocket.chat/string-helpers": next "@rocket.chat/ui-contexts": "workspace:^" "@types/ejson": ^2.2.0 "@types/eslint": ^8 @@ -4866,10 +4844,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/emitter@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/emitter@npm:0.31.18" - checksum: 86982c8962ed7691aaf791b9d18b9ec7024836c53d90a9580d6a5a7f5f2fcee5ca39dbe38919d83b693bb2cc240bbba1d5f53e2a68cd7b079a401112f5150bf4 +"@rocket.chat/emitter@npm:next": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/emitter@npm:0.31.19-dev.19" + checksum: 1c87c6007779d3253da57bf7917d02c645fa9d9f90eb2b4748fd8345d87d2555b81474ae33720a1f52504eecaa777c6bd8eef3a8348d2bdf8821463164472045 languageName: node linkType: hard @@ -4972,21 +4950,21 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-hooks@npm:0.31.18, @rocket.chat/fuselage-hooks@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage-hooks@npm:0.31.18" +"@rocket.chat/fuselage-hooks@npm:next, @rocket.chat/fuselage-hooks@npm:~0.32.0-dev.64": + version: 0.32.0-dev.64 + resolution: "@rocket.chat/fuselage-hooks@npm:0.32.0-dev.64" dependencies: use-sync-external-store: ~1.2.0 peerDependencies: "@rocket.chat/fuselage-tokens": "*" react: ^17.0.2 - checksum: d0b5815fc7a78b06e630295b4314b17e29405f54b675b68dab124e7d936e39643996c8bd0e91e149eded755c1725c2284ea874318e3c6c248996f2084a8036df + checksum: 0b474994184e05a6c0d3be4f72140fd6db12066c6a57bced560887e826a9da302557cf6690f2cf3a50a83664e657633981bebb291033f6fc4ff48a8e90b6dd46 languageName: node linkType: hard -"@rocket.chat/fuselage-polyfills@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage-polyfills@npm:0.31.18" +"@rocket.chat/fuselage-polyfills@npm:next": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/fuselage-polyfills@npm:0.31.19-dev.19" dependencies: "@juggle/resize-observer": ^3.3.1 clipboard-polyfill: ^3.0.3 @@ -4994,13 +4972,13 @@ __metadata: focus-visible: ^5.2.0 focus-within-polyfill: ^5.2.1 new-event-polyfill: ^1.0.1 - checksum: 09b78659f08da58099571f838198486fb49e87098197721db5c87617bd70b85707b340c5ab3d1957503a89d46384371db676a7db1674af2ba0ca21f4c170508a + checksum: 3ae50bd495c88f6ff74c88cc4ce9e3fa9d31be08bce94ee7769323c05aa544af8fc494088d31f2049eab8c953b55f0281823424c7f853cff6ce5427f9989092a languageName: node linkType: hard -"@rocket.chat/fuselage-toastbar@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage-toastbar@npm:0.31.18" +"@rocket.chat/fuselage-toastbar@npm:next": + version: 0.32.0-dev.125 + resolution: "@rocket.chat/fuselage-toastbar@npm:0.32.0-dev.125" peerDependencies: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" @@ -5008,29 +4986,22 @@ __metadata: "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 - checksum: 98cb0efeb6f5ef5897d7755ffe66b205de0cd7838cb1aba835f3c5a08565856e50a08e980c41bcd5ad3722909024b73c03b0a0bb23b6b93e975cc00da4e9b210 - languageName: node - linkType: hard - -"@rocket.chat/fuselage-tokens@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage-tokens@npm:0.31.18" - checksum: b8dc4c9ca4abef4c4d95c5b927b2f5a6a4aa38cee4cb22c58425f7423edf5424966de161af2686904be6c6fb4da0f3174b13e10872ee3a05cc6367aaadf0153a + checksum: 73d447a894080af1d174d6ee89f12b4b6c52b668e76bcd008b225b0aec16c9247b07553638e6c21cb5437dd8dcb83d6a051ce1f3ff6f2e7b26b2b8eb2b9e1efd languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.98": - version: 0.32.0-dev.98 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.98" - checksum: 715dc2d4e5ec4e6ca2bbfdc6eb8844c0eb7135ad020b60f4c2f80c864dd8b2a9823b2ac8328dfdcf380bdeec67a05595d159eb0fd4e09fd80b81107ca26462b6 +"@rocket.chat/fuselage-tokens@npm:next, @rocket.chat/fuselage-tokens@npm:~0.32.0-dev.101": + version: 0.32.0-dev.101 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.101" + checksum: 516aeecbfb84311bf819a867447a189467c7058cf65affe1e4b43ebde48b1d7db3714cbbcb4d1e8f4cc45369338f643ecd6d9e3d8b7b076ec12319492e1d8979 languageName: node linkType: hard -"@rocket.chat/fuselage-ui-kit@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/fuselage-ui-kit@npm:0.31.18" +"@rocket.chat/fuselage-ui-kit@npm:next": + version: 0.32.0-dev.101 + resolution: "@rocket.chat/fuselage-ui-kit@npm:0.32.0-dev.101" dependencies: - "@rocket.chat/ui-kit": ^0.31.18 + "@rocket.chat/ui-kit": ~0.32.0-dev.86 tslib: ^2.3.1 peerDependencies: "@rocket.chat/fuselage": "*" @@ -5040,7 +5011,7 @@ __metadata: "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 - checksum: ad5a028cf8a3cc243145ee01183147796c1c1dacdd43bb15bec2569ebbecb2e96857a712dadc49f3513349bfef60b2c6c20ced638be4f846f6061a74c76892a9 + checksum: 806b0192e1c56c4309ebe5d00f710f09a52f3c8e110025741d684057afa46ca104872e7c8c58af5a543fc992537f9ff6fd2438982de40edcef96a4a065abab44 languageName: node linkType: hard @@ -5051,12 +5022,12 @@ __metadata: "@rocket.chat/apps-engine": ~1.30.0 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": 0.31.18 - "@rocket.chat/fuselage-polyfills": 0.31.18 - "@rocket.chat/icons": 0.31.18 + "@rocket.chat/fuselage-hooks": next + "@rocket.chat/fuselage-polyfills": next + "@rocket.chat/icons": next "@rocket.chat/prettier-config": next - "@rocket.chat/styled": 0.31.18 - "@rocket.chat/ui-kit": 0.31.18 + "@rocket.chat/styled": next + "@rocket.chat/ui-kit": next "@storybook/addon-essentials": ~6.5.10 "@storybook/addons": ~6.5.10 "@storybook/builder-webpack5": ~6.5.10 @@ -5091,14 +5062,14 @@ __metadata: linkType: soft "@rocket.chat/fuselage@npm:next": - version: 0.32.0-dev.148 - resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.148" - dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.16 - "@rocket.chat/css-supports": ~0.31.19-dev.16 - "@rocket.chat/fuselage-tokens": ~0.32.0-dev.98 - "@rocket.chat/memo": ~0.31.19-dev.16 - "@rocket.chat/styled": ~0.31.19-dev.16 + version: 0.32.0-dev.151 + resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.151" + dependencies: + "@rocket.chat/css-in-js": ~0.31.19-dev.19 + "@rocket.chat/css-supports": ~0.31.19-dev.19 + "@rocket.chat/fuselage-tokens": ~0.32.0-dev.101 + "@rocket.chat/memo": ~0.31.19-dev.19 + "@rocket.chat/styled": ~0.31.19-dev.19 invariant: ^2.2.4 react-keyed-flatten-children: ^1.3.0 peerDependencies: @@ -5108,7 +5079,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: e8128de26fc98c57669ad56dbff67c34759c247c299b16c49188bf82260e65f96a90859051566ef52e301735657c9d564b02200ff5ff0b9e0437aae876564111 + checksum: 9d9d278e47b3e07095d6fb3ff988c1aafa5da802444069d9898b5d6fc706685949a939fdd9f336c6eac6a7d853c83ec15762158a9a11faab3c1a44aedf2e4a77 languageName: node linkType: hard @@ -5119,11 +5090,11 @@ __metadata: "@babel/core": ^7.18.9 "@mdx-js/react": ^1.6.22 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/css-in-js": 0.31.18 + "@rocket.chat/css-in-js": next "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-tokens": 0.31.18 - "@rocket.chat/message-parser": 0.31.18 - "@rocket.chat/styled": 0.31.18 + "@rocket.chat/fuselage-tokens": next + "@rocket.chat/message-parser": next + "@rocket.chat/styled": next "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" "@storybook/addon-actions": ~6.5.10 @@ -5173,22 +5144,22 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/icons@npm:0.31.18" - checksum: a16e4433a04d09501037ea172ef838b347bd0bdfd2e6e5b3ff360b160691ae10c658726e9147302a85e13b18e61b480329673e1e93ec61e5f60ec1eb7b9b61fb +"@rocket.chat/icons@npm:next": + version: 0.32.0-dev.133 + resolution: "@rocket.chat/icons@npm:0.32.0-dev.133" + checksum: 7355dcad41052d849ba739efed5c4b05a76a09ea23db542a5b9cca14730e2b4a24f9502e161e60eebf876e4e3e65ceeb6203bea5d579235a00a2ed04f5c62531 languageName: node linkType: hard "@rocket.chat/layout@npm:next": - version: 0.32.0-dev.3 - resolution: "@rocket.chat/layout@npm:0.32.0-dev.3" + version: 0.32.0-dev.34 + resolution: "@rocket.chat/layout@npm:0.32.0-dev.34" peerDependencies: "@rocket.chat/fuselage": "*" react: 17.0.2 react-dom: 17.0.2 react-i18next: ~11.15.4 - checksum: 99777244b14a8f52580d06850dbc9aa9c7088bde6331f3e9503b8000a025bef4aba3228e3a3da08c2f9ba38b41f582bb1fce04becc2a603ef6351708121ffe4d + checksum: 303c91323119322d0e97852e639f632a7baec7b1fd6ce2406df7b8df2ebf56f5451b788d060f074df29c2dec0232de465a0d1a2bab2be9fc2531670a656ab23f languageName: node linkType: hard @@ -5199,10 +5170,10 @@ __metadata: "@babel/eslint-parser": ^7.18.9 "@babel/preset-env": ^7.18.9 "@rocket.chat/eslint-config": ^0.4.0 - "@rocket.chat/fuselage-tokens": 0.31.18 - "@rocket.chat/logo": 0.31.18 + "@rocket.chat/fuselage-tokens": next + "@rocket.chat/logo": next "@rocket.chat/sdk": ^1.0.0-alpha.42 - "@rocket.chat/ui-kit": ^0.31.16 + "@rocket.chat/ui-kit": next "@storybook/addon-actions": ~6.5.10 "@storybook/addon-backgrounds": ~6.5.10 "@storybook/addon-essentials": ~6.5.10 @@ -5275,38 +5246,31 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/logo@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/logo@npm:0.31.18" +"@rocket.chat/logo@npm:next": + version: 0.32.0-dev.101 + resolution: "@rocket.chat/logo@npm:0.32.0-dev.101" dependencies: - "@rocket.chat/fuselage-hooks": ^0.31.18 - "@rocket.chat/styled": ^0.31.18 + "@rocket.chat/fuselage-hooks": ~0.32.0-dev.64 + "@rocket.chat/styled": ~0.31.19-dev.19 tslib: ^2.3.1 peerDependencies: react: 17.0.2 react-dom: 17.0.2 - checksum: b10b18f60541b7c74b0808cb54d6f0ad4c69a0eaff3f27e3029d7ffb0eb57c88249df2d936e0f2b5f158c87a079d982c16634797c3452fc6602d89177b85dded - languageName: node - linkType: hard - -"@rocket.chat/memo@npm:0.31.18, @rocket.chat/memo@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/memo@npm:0.31.18" - checksum: ed0eab0a16cc807d1bbac7b1d0dbe50592e429602176bab3d3002c6da13b4017cd35e2b9348d804cc12a497a9e11eb64a1cad1e6dbd4c74986c75435d00d5869 + checksum: 857dead0323356f28a66d525331a7485d8401c896e7ac4bf8ab6c481cd6bbd4b3a1194079e48c48fa87bca93b9b2c0578e13e218ab6e0babc38e5fab89bb0a06 languageName: node linkType: hard -"@rocket.chat/memo@npm:~0.31.19-dev.16": - version: 0.31.19-dev.16 - resolution: "@rocket.chat/memo@npm:0.31.19-dev.16" - checksum: 3b8c587175e88c06dad70defca1727972b503f6dbee4992630c92580ea2f74abfe14bda91608ab690a2bdb4da42ab93f9770f1818a26c9bcd2eb47ccacf35f53 +"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.19-dev.19": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/memo@npm:0.31.19-dev.19" + checksum: 90d66af5499c6d49ff64a695faaebf8ed5f073c78472ff8f5f3432697fc3ef6065914ac302f64cb88d840d0816cba7d0148461bd15157a1678f886fe3e5d7adf languageName: node linkType: hard -"@rocket.chat/message-parser@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/message-parser@npm:0.31.18" - checksum: 7d2025e379bf4e78b007bfd6be05ce499e7a573a15ce75d4c183236677313ad985f182f5a3e92257fb12ff5ba5190d89b32abe531dafb310f8198c1c13758d4f +"@rocket.chat/message-parser@npm:next": + version: 0.32.0-dev.99 + resolution: "@rocket.chat/message-parser@npm:0.32.0-dev.99" + checksum: 92f08cc2f04e995030a47e5de871eb6b1cb36e45253a63ea812d2adc331d9fba9dc68ca484a87cc010bea0e7b37d877f35fe86db6b809d2f008aee64b72c5d7e languageName: node linkType: hard @@ -5336,35 +5300,35 @@ __metadata: "@rocket.chat/apps-engine": 1.34.0 "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/css-in-js": 0.31.18 - "@rocket.chat/emitter": 0.31.18 + "@rocket.chat/css-in-js": next + "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": 0.31.18 - "@rocket.chat/fuselage-polyfills": 0.31.18 - "@rocket.chat/fuselage-toastbar": 0.31.18 - "@rocket.chat/fuselage-tokens": 0.31.18 - "@rocket.chat/fuselage-ui-kit": 0.31.18 + "@rocket.chat/fuselage-hooks": next + "@rocket.chat/fuselage-polyfills": next + "@rocket.chat/fuselage-toastbar": next + "@rocket.chat/fuselage-tokens": next + "@rocket.chat/fuselage-ui-kit": next "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": 0.31.18 + "@rocket.chat/icons": next "@rocket.chat/layout": next "@rocket.chat/livechat": "workspace:^" - "@rocket.chat/logo": 0.31.18 - "@rocket.chat/memo": 0.31.18 - "@rocket.chat/message-parser": 0.31.18 + "@rocket.chat/logo": next + "@rocket.chat/memo": next + "@rocket.chat/message-parser": next "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 - "@rocket.chat/onboarding-ui": 0.31.18 + "@rocket.chat/onboarding-ui": next "@rocket.chat/poplib": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": 0.31.18 + "@rocket.chat/string-helpers": next "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" - "@rocket.chat/ui-kit": 0.31.18 + "@rocket.chat/ui-kit": next "@rocket.chat/ui-video-conf": "workspace:^" "@settlin/spacebars-loader": ^1.0.9 "@slack/rtm-api": ^6.0.0 @@ -5672,9 +5636,9 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/onboarding-ui@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/onboarding-ui@npm:0.31.18" +"@rocket.chat/onboarding-ui@npm:next": + version: 0.32.0-dev.151 + resolution: "@rocket.chat/onboarding-ui@npm:0.32.0-dev.151" dependencies: i18next: ~21.6.11 react-hook-form: ~7.27.0 @@ -5690,7 +5654,7 @@ __metadata: react: 17.0.2 react-dom: 17.0.2 react-i18next: ~11.15.4 - checksum: f8067fe4f426103dbe4022525206118df27c927667a5f02927000db92f1bba5739129fd03462ab81ac56496af2df693cd60797ac230c1aae1a37905217c8bd7a + checksum: 60bc5b214020138de8991315342fe0ee8fcfbc5d08479ec37962953b8bc6cf800618967792bdf4c6c7095e1c6924419c7deb746f00b3e11668ec0ebaa0ea2564 languageName: node linkType: hard @@ -5722,8 +5686,8 @@ __metadata: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/message-parser": 0.31.18 - "@rocket.chat/ui-kit": 0.31.18 + "@rocket.chat/message-parser": next + "@rocket.chat/ui-kit": next "@types/jest": ^27.4.1 ajv: ^8.11.0 eslint: ^8.20.0 @@ -5747,56 +5711,34 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/string-helpers@npm:0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/string-helpers@npm:0.31.18" +"@rocket.chat/string-helpers@npm:next": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/string-helpers@npm:0.31.19-dev.19" dependencies: tslib: ^2.3.1 - checksum: 82af239036075034112329521cd12813c340aa21055b95da5547b452e60d334cf3ee68a18cdc837a860bd2ffa55ed835cb8c3bdee0ba3daa7233a494e2152015 + checksum: 9546a53512c67367dda6c9afd0eac8a2d5eda6bbd5a590b32359223954d863a32639249fab6845c1bf16afb549d71835eef33bea89b7d347130e585414d11dbc languageName: node linkType: hard -"@rocket.chat/styled@npm:0.31.18, @rocket.chat/styled@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/styled@npm:0.31.18" +"@rocket.chat/styled@npm:next, @rocket.chat/styled@npm:~0.31.19-dev.19": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/styled@npm:0.31.19-dev.19" dependencies: - "@rocket.chat/css-in-js": ^0.31.18 + "@rocket.chat/css-in-js": ~0.31.19-dev.19 tslib: ^2.3.1 - checksum: 8b765baa25fa13ab57a79a2fcce0c85477b898acda47651f41660be83a13b91209a9e87e3dea9857bf8e16e4f2224220c9f5a19de8e02aeab9502c995fd24b4a + checksum: 5f31d307c126aa003581fb6d2a3ac57d883268444bc02c7c9c4222f24747dadbbf84586b3fe28628f62e2f177311e0fc4a977e27fece162aa336aa53285c4596 languageName: node linkType: hard -"@rocket.chat/styled@npm:~0.31.19-dev.16": - version: 0.31.19-dev.16 - resolution: "@rocket.chat/styled@npm:0.31.19-dev.16" +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.19": + version: 0.31.19-dev.19 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.19" dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.16 - tslib: ^2.3.1 - checksum: 685654d2f8c61738ef0557179e9cc2ee9501c762bb8ead5ebb297f5c44756a5e055de5069a9ae945e328ca60a6eb69cede0697adf72fd6aea00134af19f110fb - languageName: node - linkType: hard - -"@rocket.chat/stylis-logical-props-middleware@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.18" - dependencies: - "@rocket.chat/css-supports": ^0.31.18 + "@rocket.chat/css-supports": ~0.31.19-dev.19 tslib: ^2.3.1 peerDependencies: stylis: 4.0.10 - checksum: 24aa54bc1ad2735c3fafa606de3f49199cc189ecebe35c6023105a9e1e8f1bab445b382b7766d0b2f9884f275cabea39e99884b7c5b25a879b4a7b6d0af2ce3e - languageName: node - linkType: hard - -"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.16": - version: 0.31.19-dev.16 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.16" - dependencies: - "@rocket.chat/css-supports": ~0.31.19-dev.16 - tslib: ^2.3.1 - peerDependencies: - stylis: 4.0.10 - checksum: 5de7d4d994e3d9ceb48eff8e2566276268a20e78cacc12f9298846adf637b97c728b9b78fb23dfc446783db71a311af9cfb2e8bfd63792754f37bef233e438b3 + checksum: 9e78c7411de4c1f52b842a35c5060e8122814ccbcc5302878bbab5201baa5a6521478a0a70f31e7747b3782960d35c2f8ce39dee212e4270db2ab1b019766bce languageName: node linkType: hard @@ -5804,10 +5746,10 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" dependencies: - "@rocket.chat/css-in-js": 0.31.18 + "@rocket.chat/css-in-js": next "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": 0.31.18 - "@rocket.chat/icons": 0.31.18 + "@rocket.chat/fuselage-hooks": next + "@rocket.chat/icons": next "@rocket.chat/ui-contexts": "workspace:~" "@storybook/addon-actions": ~6.5.10 "@storybook/addon-docs": ~6.5.10 @@ -5852,8 +5794,8 @@ __metadata: resolution: "@rocket.chat/ui-contexts@workspace:packages/ui-contexts" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": 0.31.18 - "@rocket.chat/fuselage-hooks": 0.31.18 + "@rocket.chat/emitter": next + "@rocket.chat/fuselage-hooks": next "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/react": ^17.0.47 @@ -5875,17 +5817,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/ui-kit@npm:0.31.18, @rocket.chat/ui-kit@npm:^0.31.18": - version: 0.31.18 - resolution: "@rocket.chat/ui-kit@npm:0.31.18" - checksum: 857c474a86a0a3cdc2b0855f52b8a97ff925d97c0a7348e18abd4268cc064588bb4b3c6faa5ef86514ad464134c90b955235eef3428f85e585be99688ad4d7f5 - languageName: node - linkType: hard - -"@rocket.chat/ui-kit@npm:^0.31.16": - version: 0.31.16 - resolution: "@rocket.chat/ui-kit@npm:0.31.16" - checksum: 08e161a629476338aacbe9a9b46feeb372f052c4548999b2611894cf9778b1dab4ac48586c43827345d8024c86ddea509022d627bbb4bb39ca1b667172260a38 +"@rocket.chat/ui-kit@npm:next, @rocket.chat/ui-kit@npm:~0.32.0-dev.86": + version: 0.32.0-dev.86 + resolution: "@rocket.chat/ui-kit@npm:0.32.0-dev.86" + checksum: 234a8a384cdfd881074bfd357de7f3805a6af1798cc73d66fd89735abad7d6d0414f1bbe674cc805c0b8c6cf897364b257027c383cc3e8be4bb26207485332a7 languageName: node linkType: hard @@ -5893,11 +5828,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-video-conf@workspace:packages/ui-video-conf" dependencies: - "@rocket.chat/css-in-js": 0.31.18 + "@rocket.chat/css-in-js": next "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": 0.31.18 - "@rocket.chat/styled": 0.31.18 + "@rocket.chat/fuselage-hooks": next + "@rocket.chat/styled": next "@types/jest": ^27.4.1 eslint: ^8.20.0 eslint-plugin-react: ^7.30.1 @@ -21842,7 +21777,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d + checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 languageName: node linkType: hard @@ -29049,14 +28984,14 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": 0.31.18 - "@rocket.chat/icons": 0.31.18 - "@rocket.chat/message-parser": 0.31.18 + "@rocket.chat/emitter": next + "@rocket.chat/icons": next + "@rocket.chat/message-parser": next "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": 0.31.18 - "@rocket.chat/ui-kit": 0.31.18 + "@rocket.chat/string-helpers": next + "@rocket.chat/ui-kit": next "@types/cookie": ^0.5.1 "@types/cookie-parser": ^1.4.3 "@types/ejson": ^2.2.0 -- GitLab From effda0e6f3047d218b11b56279b6d8abccb2126f Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Thu, 15 Sep 2022 10:22:28 +0530 Subject: [PATCH 036/107] [FIX] Unable to send native video recording to Whatsapp (#26669) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .../app/ui-message/client/messageBox/messageBoxActions.ts | 3 ++- apps/meteor/app/ui-vrecord/client/VRecDialog.js | 7 +++++++ apps/meteor/app/ui-vrecord/client/vrecord.js | 4 +++- apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.js | 5 +++-- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts index 89f2238174c..3b808b75222 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts @@ -18,7 +18,8 @@ messageBox.actions.add('Create_new', 'Video_message', { settings.get('FileUpload_Enabled') && settings.get('Message_VideoRecorderEnabled') && (!settings.get('FileUpload_MediaTypeBlackList') || !settings.get('FileUpload_MediaTypeBlackList').match(/video\/webm|video\/\*/i)) && - (!settings.get('FileUpload_MediaTypeWhiteList') || settings.get('FileUpload_MediaTypeWhiteList').match(/video\/webm|video\/\*/i)), + (!settings.get('FileUpload_MediaTypeWhiteList') || settings.get('FileUpload_MediaTypeWhiteList').match(/video\/webm|video\/\*/i)) && + window.MediaRecorder.isTypeSupported('video/webm; codecs=vp8,opus'), action: ({ rid, tmid, messageBox }) => (VRecDialog.opened ? VRecDialog.close() : VRecDialog.open(messageBox, { rid, tmid })), }); diff --git a/apps/meteor/app/ui-vrecord/client/VRecDialog.js b/apps/meteor/app/ui-vrecord/client/VRecDialog.js index a6c7a7bdd4d..a494a922ad1 100644 --- a/apps/meteor/app/ui-vrecord/client/VRecDialog.js +++ b/apps/meteor/app/ui-vrecord/client/VRecDialog.js @@ -1,7 +1,9 @@ import { Blaze } from 'meteor/blaze'; import { Template } from 'meteor/templating'; +import { dispatchToastMessage } from '../../../client/lib/toast'; import { VideoRecorder } from '../../ui'; +import { t } from '../../utils/client'; export const VRecDialog = new (class { opened = false; @@ -17,6 +19,11 @@ export const VRecDialog = new (class { this.init(); } + if (!window.MediaRecorder.isTypeSupported('video/webm; codecs=vp8,opus')) { + dispatchToastMessage({ type: 'error', message: t('Browser_does_not_support_recording_video') }); + return; + } + this.dialogView.templateInstance().update({ rid, tmid, diff --git a/apps/meteor/app/ui-vrecord/client/vrecord.js b/apps/meteor/app/ui-vrecord/client/vrecord.js index 8c8162e673c..fb7fb34553c 100644 --- a/apps/meteor/app/ui-vrecord/client/vrecord.js +++ b/apps/meteor/app/ui-vrecord/client/vrecord.js @@ -77,7 +77,9 @@ Template.vrecDialog.events({ 'click .vrec-dialog .ok'(e, instance) { const [rid, tmid, input] = [instance.rid.get(), instance.tmid.get(), instance.input.get()]; const cb = (blob) => { - fileUpload([{ file: blob, type: 'video', name: `${TAPi18n.__('Video record')}.webm` }], input, { rid, tmid }); + const fileName = `${TAPi18n.__('Video record')}.webm`; + const file = new File([blob], fileName, { type: 'video/webm' }); + fileUpload([{ file, type: 'video/webm', name: fileName }], input, { rid, tmid }); VRecDialog.close(); }; VideoRecorder.stop(cb); diff --git a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.js b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.js index 0813d06636e..18840c2277a 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.js +++ b/apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.js @@ -40,7 +40,8 @@ export const VideoRecorder = new (class VideoRecorder { if (this.stream == null) { return; } - this.mediaRecorder = new MediaRecorder(this.stream, { type: 'video/webm' }); + + this.mediaRecorder = new window.MediaRecorder(this.stream, { mimeType: 'video/webm; codecs=vp8,opus' }); this.mediaRecorder.ondataavailable = (blobev) => { this.chunks.push(blobev.data); if (!this.recordingAvailable.get()) { @@ -99,7 +100,7 @@ export const VideoRecorder = new (class VideoRecorder { this.recordingAvailable.set(false); if (cb && this.chunks) { - const blob = new Blob(this.chunks, { type: 'video/webm' }); + const blob = new Blob(this.chunks); cb(blob); } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 8d556ccb29a..6372864192d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -742,6 +742,7 @@ "Browse_Files": "Browse Files", "Browser_does_not_support_audio_element": "Your browser does not support the audio element.", "Browser_does_not_support_video_element": "Your browser does not support the video element.", + "Browser_does_not_support_recording_video": "Your browser does not support recording video", "Bugsnag_api_key": "Bugsnag API Key", "Build_Environment": "Build Environment", "bulk-register-user": "Bulk Create Users", -- GitLab From 18b7b4b36a4bea791a7479e486e0d5af99e26ba3 Mon Sep 17 00:00:00 2001 From: Jorge Cervantes <jcervantes@grupoesquimal.mx> Date: Thu, 15 Sep 2022 09:00:24 -0500 Subject: [PATCH 037/107] [FIX] Incorrect filter on contact history search (#26813) Co-authored-by: Debdut Chakraborty <debdut.chakraborty@rocket.chat> Co-authored-by: Kevin Aleman <kaleman960@gmail.com> --- apps/meteor/app/livechat/server/api/lib/visitors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.js b/apps/meteor/app/livechat/server/api/lib/visitors.js index cfa62ead68d..b16bb4eeea4 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.js +++ b/apps/meteor/app/livechat/server/api/lib/visitors.js @@ -86,13 +86,13 @@ export async function searchChats({ const [total] = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({ visitorId, open: closedChatsOnly !== 'true', - served: served !== 'true', + served: served === 'true', searchText, onlyCount: true, }).toArray(); const cursor = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({ visitorId, - open: closedChatsOnly === 'true', + open: closedChatsOnly !== 'true', served: served === 'true', searchText, options, -- GitLab From 5bfcc21dc92ac0866b5a838560e12d2ce9616a11 Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Thu, 15 Sep 2022 12:24:59 -0300 Subject: [PATCH 038/107] [FIX] Asset settings description not showing on admin (#26755) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .../theme/client/imports/general/base_old.css | 49 +++++++++++++++++++ .../client/views/admin/settings/Setting.tsx | 5 +- .../settings/inputs/AssetSettingInput.tsx | 7 ++- .../rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/tests/e2e/page-objects/admin.ts | 12 +++++ apps/meteor/tests/e2e/settings-assets.spec.ts | 29 +++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/tests/e2e/settings-assets.spec.ts diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index ee013f04e46..64a75e9a58e 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -827,6 +827,55 @@ /* MAIN CONTENT + MAIN PAGES */ +.rc-old .page-settings { + & .settings-file-preview { + display: flex; + align-items: center; + + & input[type='file'] { + position: absolute !important; + z-index: 10000; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + cursor: pointer; + + opacity: 0; + + & * { + cursor: pointer; + } + } + + & .preview { + overflow: hidden; + + width: 100px; + height: 40px; + + margin-right: 0.75rem; + + border-width: var(--input-border-width); + border-color: var(--input-border-color); + border-radius: var(--input-border-radius); + + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + + &.no-file { + display: flex; + + align-items: center; + justify-content: center; + } + } + } +} + .rc-old .page-list { & .search { margin-bottom: 12px; diff --git a/apps/meteor/client/views/admin/settings/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting.tsx index e200d02a1bc..b70212c22c9 100644 --- a/apps/meteor/client/views/admin/settings/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting.tsx @@ -88,12 +88,13 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr const { _id, disabled, readonly, type, packageValue, i18nLabel, i18nDescription, alert, invisible } = setting; - const label = (t.has(i18nLabel) && t(i18nLabel)) || (t.has(_id) && t(_id)); + const label = (t.has(i18nLabel) && t(i18nLabel)) || (t.has(_id) && t(_id)) || i18nLabel || _id; + const hint = useMemo( () => (t.has(i18nDescription) ? <MarkdownText variant='inline' preserveHtml content={t(i18nDescription)} /> : undefined), [i18nDescription, t], ); - const callout = useMemo(() => (alert && t.has(alert) ? <span dangerouslySetInnerHTML={{ __html: t(alert) }} /> : undefined), [alert, t]); + const callout = useMemo(() => alert && <span dangerouslySetInnerHTML={{ __html: t.has(alert) ? t(alert) : alert }} />, [alert, t]); const shouldDisableEnterprise = setting.enterprise && !isEnterprise; diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx index 19cf15c2121..ab8e1f54df8 100644 --- a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx @@ -63,7 +63,12 @@ function AssetSettingInput({ _id, label, value, asset, fileConstraints }: AssetS <Field.Row> <div className='settings-file-preview'> {value?.url ? ( - <div className='preview' style={{ backgroundImage: `url(${value.url}?_dc=${Random.id()})` }} /> + <div + className='preview' + style={{ backgroundImage: `url(${value.url}?_dc=${Random.id()})` }} + role='img' + aria-label={t('Asset_preview')} + /> ) : ( <div className='preview no-file background-transparent-light secondary-font-color'> <Icon name='upload' /> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 6372864192d..8f9608a81eb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -592,6 +592,7 @@ "Are_you_sure_you_want_to_disable_Facebook_integration": "Are you sure you want to disable Facebook integration?", "Assets": "Assets", "Assets_Description": "Modify your workspace's logo, icon, favicon and more.", + "Asset_preview": "Asset preview", "Assign_admin": "Assigning admin", "Assign_new_conversations_to_bot_agent": "Assign new conversations to bot agent", "Assign_new_conversations_to_bot_agent_description": "The routing system will attempt to find a bot agent before addressing new conversations to a human agent.", diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 5e1962bea6b..4aa1373195b 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -95,4 +95,16 @@ export class Admin { get btnResetRobotsFileContent(): Locator { return this.page.locator('[data-qa-reset-setting-id="Robot_Instructions_File_Content"]'); } + + get btnAssetsSettings(): Locator { + return this.page.locator('[data-qa-id="Assets"] >> role=button[name="Open"]'); + } + + get btnDeleteAssetsLogo(): Locator { + return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> role=button[name="Delete"]'); + } + + get inputAssetsLogo(): Locator { + return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> input[type="file"]'); + } } diff --git a/apps/meteor/tests/e2e/settings-assets.spec.ts b/apps/meteor/tests/e2e/settings-assets.spec.ts new file mode 100644 index 00000000000..37f1dee2ed1 --- /dev/null +++ b/apps/meteor/tests/e2e/settings-assets.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from './utils/test'; +import { Admin } from './page-objects'; + +test.use({ storageState: 'admin-session.json' }); + +test.describe.serial('settings-assets', () => { + let poAdmin: Admin; + + test.beforeEach(async ({ page }) => { + poAdmin = new Admin(page); + await page.goto('/admin/settings'); + + await poAdmin.btnAssetsSettings.click(); + + await expect(page.locator('[data-qa-type="PageHeader-title"]')).toHaveText('Assets'); + }); + + test('expect upload and delete asset and label should be visible', async ({ page }) => { + await expect(page.locator('[title="Assets_logo"]')).toHaveText('logo (svg, png, jpg)'); + + await poAdmin.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); + + await expect(page.locator('role=img[name="Asset preview"]')).toBeVisible(); + + await poAdmin.btnDeleteAssetsLogo.click(); + + await expect(page.locator('role=img[name="Asset preview"]')).not.toBeVisible(); + }); +}); -- GitLab From c604e4be044fb39cc68bab4c84b3fafc48f6d6bb Mon Sep 17 00:00:00 2001 From: Douglas Fabris <devfabris@gmail.com> Date: Thu, 15 Sep 2022 13:57:42 -0300 Subject: [PATCH 039/107] Chore: Move Header to ui-client (#26757) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/app/livestream/client/tabBar.tsx | 10 +- .../app/threads/client/flextab/threadlist.tsx | 8 +- apps/meteor/app/videobridge/client/tabBar.tsx | 1 + .../client/components/Header/Avatar.tsx | 7 - .../client/components/Header/Button.tsx | 6 - apps/meteor/client/components/Header/Row.tsx | 6 - .../meteor/client/components/Header/State.tsx | 6 - .../client/components/Header/Subtitle.tsx | 6 - .../meteor/client/components/Header/Title.tsx | 6 - .../client/components/Header/ToolBox.tsx | 6 - .../client/components/Page/PageHeader.tsx | 2 +- .../client/components/avatar/BaseAvatar.tsx | 4 +- .../client/views/room/Header/Header.tsx | 2 +- .../views/room/Header/HeaderIconWithRoom.tsx | 2 +- .../room/Header/Omnichannel/BackButton.tsx | 5 +- .../Omnichannel/OmnichannelRoomHeader.tsx | 2 +- .../Omnichannel/QuickActions/QuickActions.tsx | 17 ++- .../QuickActions/hooks/useQuickActions.tsx | 6 +- .../Header/Omnichannel/VoipRoomHeader.tsx | 2 +- .../client/views/room/Header/ParentRoom.tsx | 2 +- .../Header/ParentRoomWithEndpointData.tsx | 2 +- .../client/views/room/Header/ParentTeam.tsx | 2 +- .../client/views/room/Header/RoomHeader.tsx | 72 ++++++----- .../client/views/room/Header/RoomTitle.tsx | 4 +- .../views/room/Header/ToolBox/ToolBox.tsx | 48 +++++-- .../views/room/Header/icons/Encrypted.js | 3 +- .../views/room/Header/icons/Favorite.js | 3 +- .../views/room/Header/icons/Translate.tsx | 3 +- .../meteor/client/views/room/RoomNotFound.tsx | 2 +- .../meteor/client/views/room/RoomSkeleton.tsx | 2 +- .../VideoConfPopup/VideoConfPopupRoomInfo.tsx | 2 +- .../client/views/room/lib/Toolbox/index.tsx | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 2 + .../src}/components/Header/Header.stories.tsx | 120 +++++------------- .../src}/components/Header/Header.tsx | 5 +- .../src/components/Header/HeaderAvatar.tsx | 7 + .../src/components/Header/HeaderContent.tsx | 7 +- .../components/Header/HeaderContentRow.tsx | 9 ++ .../src}/components/Header/HeaderDivider.tsx | 3 +- .../src}/components/Header/HeaderIcon.tsx | 3 +- .../src}/components/Header/HeaderLink.tsx | 5 +- .../src/components/Header/HeaderState.tsx | 7 + .../src/components/Header/HeaderSubtitle.tsx | 7 + .../src}/components/Header/HeaderTag.tsx | 3 +- .../src}/components/Header/HeaderTagIcon.tsx | 5 +- .../components/Header/HeaderTagSkeleton.tsx | 3 +- .../src/components/Header/HeaderTitle.tsx | 9 ++ .../src/components/Header/ToolBox/ToolBox.tsx | 6 + .../Header/ToolBox}/ToolBoxAction.tsx | 5 +- .../Header/ToolBox}/ToolBoxActionBadge.tsx | 3 +- .../Header/ToolBox/ToolboxDivider.tsx | 8 ++ .../src/components/Header/ToolBox/index.ts | 10 ++ .../ui-client/src}/components/Header/index.ts | 37 +++--- .../UserStatus/UserStatus.stories.tsx | 2 +- packages/ui-client/src/components/index.ts | 1 + .../{stories => }/hooks/useAutoSequence.ts | 0 56 files changed, 254 insertions(+), 263 deletions(-) delete mode 100644 apps/meteor/client/components/Header/Avatar.tsx delete mode 100644 apps/meteor/client/components/Header/Button.tsx delete mode 100644 apps/meteor/client/components/Header/Row.tsx delete mode 100644 apps/meteor/client/components/Header/State.tsx delete mode 100644 apps/meteor/client/components/Header/Subtitle.tsx delete mode 100644 apps/meteor/client/components/Header/Title.tsx delete mode 100644 apps/meteor/client/components/Header/ToolBox.tsx rename {apps/meteor/client => packages/ui-client/src}/components/Header/Header.stories.tsx (55%) rename {apps/meteor/client => packages/ui-client/src}/components/Header/Header.tsx (82%) create mode 100644 packages/ui-client/src/components/Header/HeaderAvatar.tsx rename apps/meteor/client/components/Header/Content.tsx => packages/ui-client/src/components/Header/HeaderContent.tsx (50%) create mode 100644 packages/ui-client/src/components/Header/HeaderContentRow.tsx rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderDivider.tsx (70%) rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderIcon.tsx (83%) rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderLink.tsx (76%) create mode 100644 packages/ui-client/src/components/Header/HeaderState.tsx create mode 100644 packages/ui-client/src/components/Header/HeaderSubtitle.tsx rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderTag.tsx (81%) rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderTagIcon.tsx (71%) rename {apps/meteor/client => packages/ui-client/src}/components/Header/HeaderTagSkeleton.tsx (71%) create mode 100644 packages/ui-client/src/components/Header/HeaderTitle.tsx create mode 100644 packages/ui-client/src/components/Header/ToolBox/ToolBox.tsx rename {apps/meteor/client/components/Header => packages/ui-client/src/components/Header/ToolBox}/ToolBoxAction.tsx (85%) rename {apps/meteor/client/components/Header => packages/ui-client/src/components/Header/ToolBox}/ToolBoxActionBadge.tsx (82%) create mode 100644 packages/ui-client/src/components/Header/ToolBox/ToolboxDivider.tsx create mode 100644 packages/ui-client/src/components/Header/ToolBox/index.ts rename {apps/meteor/client => packages/ui-client/src}/components/Header/index.ts (50%) rename packages/ui-client/src/{stories => }/hooks/useAutoSequence.ts (100%) diff --git a/apps/meteor/app/livestream/client/tabBar.tsx b/apps/meteor/app/livestream/client/tabBar.tsx index eb4eb766784..c662ca50e6f 100644 --- a/apps/meteor/app/livestream/client/tabBar.tsx +++ b/apps/meteor/app/livestream/client/tabBar.tsx @@ -3,9 +3,9 @@ import React, { useMemo } from 'react'; import { Option, Badge } from '@rocket.chat/fuselage'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import { isRoomFederated } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import { addAction } from '../../../client/views/room/lib/Toolbox'; -import Header from '../../../client/components/Header'; addAction('livestream', ({ room }) => { const enabled = useSetting('Livestream_enabled'); @@ -28,13 +28,13 @@ addAction('livestream', ({ room }) => { 'disabled': true, }), renderAction: (props): ReactNode => ( - <Header.ToolBoxAction {...props}> + <Header.ToolBox.Action {...props}> {isLive ? ( - <Header.Badge title={t('Livestream_live_now')} variant='danger'> + <Header.ToolBox.ActionBadge title={t('Livestream_live_now')} variant='danger'> ! - </Header.Badge> + </Header.ToolBox.ActionBadge> ) : null} - </Header.ToolBoxAction> + </Header.ToolBox.Action> ), renderOption: ({ label: { title, icon }, ...props }: any): ReactNode => ( <Option label={title} title={title} icon={icon} {...props}> diff --git a/apps/meteor/app/threads/client/flextab/threadlist.tsx b/apps/meteor/app/threads/client/flextab/threadlist.tsx index e8cf2f0dd04..052f6b4f18c 100644 --- a/apps/meteor/app/threads/client/flextab/threadlist.tsx +++ b/apps/meteor/app/threads/client/flextab/threadlist.tsx @@ -4,9 +4,9 @@ import type { BadgeProps } from '@rocket.chat/fuselage'; import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { useSetting } from '@rocket.chat/ui-contexts'; +import { Header } from '@rocket.chat/ui-client'; import { addAction } from '../../../../client/views/room/lib/Toolbox'; -import Header from '../../../../client/components/Header'; const getVariant = (tunreadUser: number, tunreadGroup: number): BadgeProps['variant'] => { if (tunreadUser > 0) { @@ -45,9 +45,9 @@ addAction('thread', (options) => { const unread = tunread > 99 ? '99+' : tunread; const variant = getVariant(tunreadUser, tunreadGroup); return ( - <Header.ToolBoxAction {...props}> - {unread > 0 && <Header.Badge variant={variant}>{unread}</Header.Badge>} - </Header.ToolBoxAction> + <Header.ToolBox.Action {...props}> + {unread > 0 && <Header.ToolBox.ActionBadge variant={variant}>{unread}</Header.ToolBox.ActionBadge>} + </Header.ToolBox.Action> ); }, order: 2, diff --git a/apps/meteor/app/videobridge/client/tabBar.tsx b/apps/meteor/app/videobridge/client/tabBar.tsx index 4baf6eb7cad..a37416af131 100644 --- a/apps/meteor/app/videobridge/client/tabBar.tsx +++ b/apps/meteor/app/videobridge/client/tabBar.tsx @@ -95,6 +95,7 @@ addAction('start-call', ({ room }) => { }), full: true, order: live ? -1 : 4, + featured: true, } : null, [groups, enableOption, live, handleOpenVideoConf, ownUser, federated], diff --git a/apps/meteor/client/components/Header/Avatar.tsx b/apps/meteor/client/components/Header/Avatar.tsx deleted file mode 100644 index 2ea57115e56..00000000000 --- a/apps/meteor/client/components/Header/Avatar.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { FC } from 'react'; - -import Button from './Button'; - -const Avatar: FC<any> = (props) => <Button width='x36' {...props} />; - -export default Avatar; diff --git a/apps/meteor/client/components/Header/Button.tsx b/apps/meteor/client/components/Header/Button.tsx deleted file mode 100644 index 2bcf6a9ed7a..00000000000 --- a/apps/meteor/client/components/Header/Button.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const Button: FC<any> = (props) => <Box mi='x4' display='flex' alignItems='center' {...props} />; - -export default Button; diff --git a/apps/meteor/client/components/Header/Row.tsx b/apps/meteor/client/components/Header/Row.tsx deleted file mode 100644 index 404990ce5ec..00000000000 --- a/apps/meteor/client/components/Header/Row.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const Row: FC<any> = (props) => <Box alignItems='center' flexShrink={1} flexGrow={1} display='flex' {...props} />; - -export default Row; diff --git a/apps/meteor/client/components/Header/State.tsx b/apps/meteor/client/components/Header/State.tsx deleted file mode 100644 index 63ac1ceb1f3..00000000000 --- a/apps/meteor/client/components/Header/State.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Icon, IconButton } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const State: FC<any> = (props) => (props.onClick ? <IconButton mini {...props} /> : <Icon size={16} name={props.icon} {...props} />); - -export default State; diff --git a/apps/meteor/client/components/Header/Subtitle.tsx b/apps/meteor/client/components/Header/Subtitle.tsx deleted file mode 100644 index 101f494d6ce..00000000000 --- a/apps/meteor/client/components/Header/Subtitle.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const Subtitle: FC<any> = (props) => <Box color='hint' fontScale='p2' withTruncatedText {...props} />; - -export default Subtitle; diff --git a/apps/meteor/client/components/Header/Title.tsx b/apps/meteor/client/components/Header/Title.tsx deleted file mode 100644 index 33f0beaf4b5..00000000000 --- a/apps/meteor/client/components/Header/Title.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const Title: FC<any> = (props) => <Box color='default' mi='x4' fontScale='h4' withTruncatedText {...props} />; - -export default Title; diff --git a/apps/meteor/client/components/Header/ToolBox.tsx b/apps/meteor/client/components/Header/ToolBox.tsx deleted file mode 100644 index 10e3a71fb6f..00000000000 --- a/apps/meteor/client/components/Header/ToolBox.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { ButtonGroup } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; - -const ToolBox: FC<any> = (props) => <ButtonGroup mi='x4' medium {...props} />; - -export default ToolBox; diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index a6a8fa3e813..b3dcb7a6cd6 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -1,9 +1,9 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; +import { Header as TemplateHeader } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useContext, FC, ComponentProps, ReactNode } from 'react'; import BurgerMenu from '../BurgerMenu'; -import TemplateHeader from '../Header'; import PageContext from './PageContext'; type PageHeaderProps = { diff --git a/apps/meteor/client/components/avatar/BaseAvatar.tsx b/apps/meteor/client/components/avatar/BaseAvatar.tsx index 2bd37d6f283..beec75d8c9f 100644 --- a/apps/meteor/client/components/avatar/BaseAvatar.tsx +++ b/apps/meteor/client/components/avatar/BaseAvatar.tsx @@ -7,10 +7,10 @@ const BaseAvatar: FC<BaseAvatarProps> = ({ size, ...props }) => { const [error, setError] = useState<unknown>(false); if (error) { - return <Skeleton variant='rect' {...props} />; + return <Skeleton aria-hidden variant='rect' {...props} />; } - return <Avatar onError={setError} size={size} {...props} />; + return <Avatar aria-hidden onError={setError} size={size} {...props} />; }; export default BaseAvatar; diff --git a/apps/meteor/client/views/room/Header/Header.tsx b/apps/meteor/client/views/room/Header/Header.tsx index 41ec19016d5..32542941a2e 100644 --- a/apps/meteor/client/views/room/Header/Header.tsx +++ b/apps/meteor/client/views/room/Header/Header.tsx @@ -1,9 +1,9 @@ import type { IRoom, IVoipRoom } from '@rocket.chat/core-typings'; +import { Header as TemplateHeader } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import React, { memo, ReactElement, useMemo } from 'react'; import BurgerMenu from '../../../components/BurgerMenu'; -import TemplateHeader from '../../../components/Header'; import DirectRoomHeader from './DirectRoomHeader'; import OmnichannelRoomHeader from './Omnichannel/OmnichannelRoomHeader'; import VoipRoomHeader from './Omnichannel/VoipRoomHeader'; diff --git a/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx b/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx index 2db18b5ce9f..fdea83b7c15 100644 --- a/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx +++ b/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx @@ -1,7 +1,7 @@ import { IRoom, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import React, { ReactElement } from 'react'; -import Header from '../../../components/Header'; import { OmnichannelRoomIcon } from '../../../components/RoomIcon/OmnichannelRoomIcon'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx b/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx index 16efedd6a33..68c6760df41 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx @@ -1,9 +1,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Header } from '@rocket.chat/ui-client'; import { useCurrentRoute, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; -import Header from '../../../../components/Header'; - export const BackButton = ({ routeName }: { routeName?: string }): ReactElement => { const t = useTranslation(); const [route = '', params] = useCurrentRoute(); @@ -20,5 +19,5 @@ export const BackButton = ({ routeName }: { routeName?: string }): ReactElement } }); - return <Header.ToolBoxAction title={t('Back')} icon='back' onClick={back} />; + return <Header.ToolBox.Action title={t('Back')} icon='back' onClick={back} />; }; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx index 553c13756b4..bf45e3d3a8f 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx @@ -1,8 +1,8 @@ +import { Header as TemplateHeader } from '@rocket.chat/ui-client'; import { useLayout, useCurrentRoute } from '@rocket.chat/ui-contexts'; import React, { FC, useMemo } from 'react'; import BurgerMenu from '../../../../components/BurgerMenu'; -import TemplateHeader from '../../../../components/Header'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; import { ToolboxActionConfig } from '../../lib/Toolbox'; import { ToolboxContext, useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 2093b0f0d26..dd1c3e75613 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -1,9 +1,9 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Box, ButtonGroup } from '@rocket.chat/fuselage'; +import { Box } from '@rocket.chat/fuselage'; +import { Header } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, FC, ComponentProps } from 'react'; -import Header from '../../../../../components/Header'; import { useQuickActions } from './hooks/useQuickActions'; type QuickActionsProps = { @@ -16,24 +16,23 @@ const QuickActions: FC<QuickActionsProps> = ({ room, className }) => { const { visibleActions, actionDefault } = useQuickActions(room); return ( - <ButtonGroup mi='x4' medium> + <Header.ToolBox aria-label={t('Omnichannel_quick_actions')}> {visibleActions.map(({ id, color, icon, title, action = actionDefault }, index) => { const props = { id, icon, color, - 'title': t(title), + title: t(title), className, index, - 'primary': false, - 'data-quick-actions': index, + primary: false, action, - 'key': id, + key: id, }; - return <Header.ToolBoxAction {...props} />; + return <Header.ToolBox.Action {...props} />; })} - </ButtonGroup> + </Header.ToolBox> ); }; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 192446a8c6e..5d18c9ee74d 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -283,10 +283,8 @@ export const useQuickActions = ( const visibleActions = actions.filter(({ id }) => hasPermissionButtons(id)); - const actionDefault = useMutableCallback((e) => { - const index = e.currentTarget.getAttribute('data-quick-actions'); - const { id } = visibleActions[index]; - openModal(id); + const actionDefault = useMutableCallback((actionId) => { + openModal(actionId); }); const getAction = useMutableCallback((id) => { diff --git a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx index de204caaeba..716e174ba65 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx @@ -1,10 +1,10 @@ import { IVoipRoom } from '@rocket.chat/core-typings'; +import { Header as TemplateHeader } from '@rocket.chat/ui-client'; import { useLayout, useCurrentRoute } from '@rocket.chat/ui-contexts'; import React, { FC, useMemo } from 'react'; import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import BurgerMenu from '../../../../components/BurgerMenu'; -import TemplateHeader from '../../../../components/Header'; import { ToolboxActionConfig } from '../../lib/Toolbox'; import { ToolboxContext, useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; import RoomHeader, { RoomHeaderProps } from '../RoomHeader'; diff --git a/apps/meteor/client/views/room/Header/ParentRoom.tsx b/apps/meteor/client/views/room/Header/ParentRoom.tsx index c82a12e8b4d..79e1f8b168d 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom.tsx @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import React, { ReactElement } from 'react'; -import Header from '../../../components/Header'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; diff --git a/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx b/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx index fd5e937f992..565b6bc9902 100644 --- a/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import React, { ReactElement } from 'react'; -import Header from '../../../components/Header'; import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; import ParentRoom from './ParentRoom'; diff --git a/apps/meteor/client/views/room/Header/ParentTeam.tsx b/apps/meteor/client/views/room/Header/ParentTeam.tsx index 1eb63ae2c21..f6be1bde2b2 100644 --- a/apps/meteor/client/views/room/Header/ParentTeam.tsx +++ b/apps/meteor/client/views/room/Header/ParentTeam.tsx @@ -1,9 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import { useUserId } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useMemo } from 'react'; -import Header from '../../../components/Header'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; diff --git a/apps/meteor/client/views/room/Header/RoomHeader.tsx b/apps/meteor/client/views/room/Header/RoomHeader.tsx index 23c822759d9..91b332c43ff 100644 --- a/apps/meteor/client/views/room/Header/RoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeader.tsx @@ -1,7 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; -import Header from '../../../components/Header'; import MarkdownText from '../../../components/MarkdownText'; import RoomAvatar from '../../../components/avatar/RoomAvatar'; import ParentRoomWithData from './ParentRoomWithData'; @@ -29,37 +30,42 @@ export type RoomHeaderProps = { }; }; -const RoomHeader: FC<RoomHeaderProps> = ({ room, topic = '', slots = {} }) => ( - <Header> - {slots?.start} - <Header.Avatar> - <RoomAvatar room={room} /> - </Header.Avatar> - {slots?.preContent} - <Header.Content> - <Header.Content.Row> - <RoomTitle room={room} /> - <Favorite room={room} /> - {room.prid && <ParentRoomWithData room={room} />} - {room.teamId && !room.teamMain && <ParentTeam room={room} />} - <Encrypted room={room} /> - <Translate room={room} /> - {slots?.insideContent} - </Header.Content.Row> - <Header.Content.Row> - <Header.Subtitle> - {topic && <MarkdownText parseEmoji={true} variant='inlineWithoutBreaks' withTruncatedText content={topic} />} - </Header.Subtitle> - </Header.Content.Row> - </Header.Content> - {slots?.posContent} - <Header.ToolBox> - {slots?.toolbox?.pre} - {slots?.toolbox?.content || <ToolBox room={room} />} - {slots?.toolbox?.pos} - </Header.ToolBox> - {slots?.end} - </Header> -); +const RoomHeader: FC<RoomHeaderProps> = ({ room, topic = '', slots = {} }) => { + const t = useTranslation(); + return ( + <Header> + {slots?.start} + <Header.Avatar> + <RoomAvatar room={room} /> + </Header.Avatar> + {slots?.preContent} + <Header.Content> + <Header.Content.Row> + <RoomTitle room={room} /> + <Favorite room={room} /> + {room.prid && <ParentRoomWithData room={room} />} + {room.teamId && !room.teamMain && <ParentTeam room={room} />} + <Encrypted room={room} /> + <Translate room={room} /> + {slots?.insideContent} + </Header.Content.Row> + {topic && ( + <Header.Content.Row> + <Header.Subtitle is='h2'> + <MarkdownText parseEmoji={true} variant='inlineWithoutBreaks' withTruncatedText content={topic} /> + </Header.Subtitle> + </Header.Content.Row> + )} + </Header.Content> + {slots?.posContent} + <Header.ToolBox aria-label={t('Toolbox_room_actions')}> + {slots?.toolbox?.pre} + {slots?.toolbox?.content || <ToolBox room={room} />} + {slots?.toolbox?.pos} + </Header.ToolBox> + {slots?.end} + </Header> + ); +}; export default RoomHeader; diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 21808862bbe..20dac05dcd1 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -1,7 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { Header } from '@rocket.chat/ui-client'; import React, { ReactElement } from 'react'; -import Header from '../../../components/Header'; import HeaderIconWithRoom from './HeaderIconWithRoom'; type RoomTitleProps = { @@ -11,7 +11,7 @@ type RoomTitleProps = { const RoomTitle = ({ room }: RoomTitleProps): ReactElement => ( <> <HeaderIconWithRoom room={room} /> - <Header.Title>{room.name}</Header.Title> + <Header.Title is='h1'>{room.name}</Header.Title> </> ); diff --git a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx index 10dc9efe6b3..7a57c61f796 100644 --- a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx +++ b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx @@ -1,11 +1,11 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Menu, Option, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Header } from '@rocket.chat/ui-client'; import { TranslationKey, useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, ReactNode, useRef, ComponentProps, ReactElement } from 'react'; // used to open the menu option by keyboard -import Header from '../../../../components/Header'; import { ToolboxActionConfig, OptionRenderer } from '../../lib/Toolbox'; import { useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; import { useTab, useTabBarOpen } from '../../providers/ToolboxProvider'; @@ -20,19 +20,21 @@ type ToolBoxProps = { }; const ToolBox = ({ className }: ToolBoxProps): ReactElement => { + const t = useTranslation(); const tab = useTab(); const openTabBar = useTabBarOpen(); const { isMobile } = useLayout(); - const t = useTranslation(); const hiddenActionRenderers = useRef<{ [key: string]: OptionRenderer }>({}); const { actions: mapActions } = useToolboxContext(); const actions = (Array.from(mapActions.values()) as ToolboxActionConfig[]).sort((a, b) => (a.order || 0) - (b.order || 0)); - const visibleActions = isMobile ? [] : actions.slice(0, 6); + const featuredActions = actions.filter((action) => action.featured); + const filteredActions = actions.filter((action) => !action.featured); + const visibleActions = isMobile ? [] : filteredActions.slice(0, 6); const hiddenActions: Record<string, ToolboxActionConfig> = Object.fromEntries( - (isMobile ? actions : actions.slice(6)) + (isMobile ? actions : filteredActions.slice(6)) .filter((item) => !item.disabled) .map((item) => { hiddenActionRenderers.current = { @@ -52,9 +54,8 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { }), ); - const actionDefault = useMutableCallback((e) => { - const index = e.currentTarget.getAttribute('data-toolbox'); - openTabBar(actions[index].id); + const actionDefault = useMutableCallback((actionId) => { + openTabBar(actionId); }); // const open = useMutableCallback((index) => { @@ -72,28 +73,49 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { // }; // }, [visibleActions.length, open]); + // TODO: Create helper for render Actions + // TODO: Add proper Vertical Divider Component + return ( <> + {featuredActions.map(({ renderAction, id, icon, title, action = actionDefault, disabled, 'data-tooltip': tooltip }, index) => { + const props = { + id, + icon, + title: t(title), + className, + index, + info: id === tab?.id, + action, + key: id, + disabled, + ...(tooltip ? { 'data-tooltip': t(tooltip as TranslationKey) } : {}), + }; + if (renderAction) { + return renderAction(props); + } + return <Header.ToolBox.Action {...props} />; + })} + {featuredActions.length > 0 && <Header.ToolBox.Divider />} {visibleActions.map(({ renderAction, id, icon, title, action = actionDefault, disabled, 'data-tooltip': tooltip }, index) => { const props = { id, icon, - 'title': t(title), + title: t(title), className, index, - 'info': id === tab?.id, - 'data-toolbox': index, + info: id === tab?.id, action, - 'key': id, + key: id, disabled, ...(tooltip ? { 'data-tooltip': t(tooltip as TranslationKey) } : {}), }; if (renderAction) { return renderAction(props); } - return <Header.ToolBoxAction {...props} />; + return <Header.ToolBox.Action {...props} />; })} - {actions.length > 6 && ( + {filteredActions.length > 6 && ( <Menu data-qa-id='ToolBox-Menu' tiny={!isMobile} diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.js b/apps/meteor/client/views/room/Header/icons/Encrypted.js index 01588065dd4..da6d69ae68f 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.js +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.js @@ -1,10 +1,9 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; +import { Header } from '@rocket.chat/ui-client'; import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; -import Header from '../../../../components/Header'; - const Encrypted = ({ room }) => { const t = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); diff --git a/apps/meteor/client/views/room/Header/icons/Favorite.js b/apps/meteor/client/views/room/Header/icons/Favorite.js index 50f2fc1ce86..ea79b04c6fb 100644 --- a/apps/meteor/client/views/room/Header/icons/Favorite.js +++ b/apps/meteor/client/views/room/Header/icons/Favorite.js @@ -1,10 +1,9 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; +import { Header } from '@rocket.chat/ui-client'; import { useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; -import Header from '../../../../components/Header'; - const Favorite = ({ room: { _id, f: favorited = false, t: type } }) => { const t = useTranslation(); const isFavoritesEnabled = useSetting('Favorite_Rooms') && ['c', 'p', 'd', 't'].includes(type); diff --git a/apps/meteor/client/views/room/Header/icons/Translate.tsx b/apps/meteor/client/views/room/Header/icons/Translate.tsx index f7de9ee3a8e..1f00ac0efcc 100644 --- a/apps/meteor/client/views/room/Header/icons/Translate.tsx +++ b/apps/meteor/client/views/room/Header/icons/Translate.tsx @@ -1,10 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import colors from '@rocket.chat/fuselage-tokens/colors'; +import { Header } from '@rocket.chat/ui-client'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, memo } from 'react'; -import Header from '../../../../components/Header'; - type TranslateProps = { room: IRoom; }; diff --git a/apps/meteor/client/views/room/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx index b4315b76225..5a39be7576d 100644 --- a/apps/meteor/client/views/room/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -1,9 +1,9 @@ import { States, StatesIcon, StatesTitle, StatesSubtitle, Box, StatesActions, StatesAction } from '@rocket.chat/fuselage'; +import { Header as TemplateHeader } from '@rocket.chat/ui-client'; import { useLayout, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; import BurgerMenu from '../../components/BurgerMenu'; -import TemplateHeader from '../../components/Header'; import RoomLayout from './layout/RoomLayout'; const RoomNotFound = (): ReactElement => { diff --git a/apps/meteor/client/views/room/RoomSkeleton.tsx b/apps/meteor/client/views/room/RoomSkeleton.tsx index 30bd33db21e..780eda051da 100644 --- a/apps/meteor/client/views/room/RoomSkeleton.tsx +++ b/apps/meteor/client/views/room/RoomSkeleton.tsx @@ -1,7 +1,7 @@ import { Skeleton, Box } from '@rocket.chat/fuselage'; +import { Header } from '@rocket.chat/ui-client'; import React, { ReactElement } from 'react'; -import Header from '../../components/Header'; import VerticalBarSkeleton from '../../components/VerticalBar/VerticalBarSkeleton'; import ComposerSkeleton from './Room/ComposerSkeleton'; import RoomLayout from './layout/RoomLayout'; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx index b463e5a6291..c5f664a5340 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/VideoConfPopupRoomInfo.tsx @@ -30,7 +30,7 @@ const VideoConfPopupRoomInfo = ({ room }: { room: IRoom }): ReactElement => { return ( <VideoConfPopupInfo avatar={avatar} icon={<RoomIcon placement='default' room={room} />}> - {room.name} + {room.fname} </VideoConfPopupInfo> ); }; diff --git a/apps/meteor/client/views/room/lib/Toolbox/index.tsx b/apps/meteor/client/views/room/lib/Toolbox/index.tsx index 39e8501174a..9074c5749fd 100644 --- a/apps/meteor/client/views/room/lib/Toolbox/index.tsx +++ b/apps/meteor/client/views/room/lib/Toolbox/index.tsx @@ -43,6 +43,7 @@ export type ToolboxActionConfig = { rid: IRoom['_id']; teamId: IRoom['teamId']; }>; + 'featured'?: boolean; }; export type ToolboxAction = ToolboxHook | ToolboxActionConfig; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 8f9608a81eb..cab1af193c6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2899,6 +2899,7 @@ "Omnichannel_On_Hold_due_to_inactivity": "The chat was automatically placed On Hold because we haven't received any reply from __guest__ in __timeout__ seconds", "Omnichannel_On_Hold_manually": "The chat was manually placed On Hold by __user__", "Omnichannel_onHold_Chat": "Place chat On-Hold", + "Omnichannel_quick_actions": "Omnichannel Quick Actions", "Livechat_online": "Omnichannel on-line", "Omnichannel_placed_chat_on_hold": "Chat On Hold: __comment__", "Livechat_Queue": "Omnichannel Queue", @@ -5428,6 +5429,7 @@ "Access_Your_Account": "Access Your Account", "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Or copy and paste this URL into a tab of your browser", "Thank_You_For_Choosing_RocketChat": "Thank you for choosing Rocket.Chat!", + "Toolbox_room_actions": "Primary Room actions", "Device_Management_Client": "Client", "Device_Management_OS": "OS", "Device_Management_Device": "Device", diff --git a/apps/meteor/client/components/Header/Header.stories.tsx b/packages/ui-client/src/components/Header/Header.stories.tsx similarity index 55% rename from apps/meteor/client/components/Header/Header.stories.tsx rename to packages/ui-client/src/components/Header/Header.stories.tsx index a8742be2b80..170ebb588ee 100644 --- a/apps/meteor/client/components/Header/Header.stories.tsx +++ b/packages/ui-client/src/components/Header/Header.stories.tsx @@ -1,22 +1,21 @@ import type { IRoom } from '@rocket.chat/core-typings'; +import { Avatar } from '@rocket.chat/fuselage'; import { SettingsContext } from '@rocket.chat/ui-contexts'; import { action } from '@storybook/addon-actions'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta } from '@storybook/react'; import React from 'react'; import Header from '.'; -import { useRoomIcon } from '../../hooks/useRoomIcon'; -import ToolBox from '../../views/room/Header/ToolBox'; -import { ActionRenderer, addAction } from '../../views/room/lib/Toolbox'; -import ToolboxProvider from '../../views/room/providers/ToolboxProvider'; -import RoomAvatar from '../avatar/RoomAvatar'; + +const avatarUrl = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; export default { title: 'Components/Header', component: Header, subcomponents: { 'Header.ToolBox': Header.ToolBox, - 'Header.ToolBoxAction': Header.ToolBoxAction, + 'Header.ToolBox.Action': Header.ToolBox.Action, 'Header.Avatar': Header.Avatar, 'Header.Content': Header.Content, 'Header.Content.Row': Header.Content.Row, @@ -73,16 +72,12 @@ const room: IRoom = { _updatedAt: new Date(), } as const; -export const ChatHeader = () => { - const icon = useRoomIcon(room); - const avatar = <RoomAvatar size='x40' room={room} />; +const avatar = <Avatar size='x40' url={avatarUrl} />; +const icon = { name: 'hash' }; +export const Default = () => { return ( <Header> - <Header.ToolBox> - <Header.ToolBoxAction icon='burger' /> - <Header.ToolBoxAction icon='back' /> - </Header.ToolBox> <Header.Avatar>{avatar}</Header.Avatar> <Header.Content> <Header.Content.Row> @@ -97,78 +92,20 @@ export const ChatHeader = () => { </Header.Content.Row> </Header.Content> <Header.ToolBox> - <Header.ToolBoxAction icon='magnifier' /> - <Header.ToolBoxAction icon='key' /> - <Header.ToolBoxAction icon='kebab' /> + <Header.ToolBox.Action icon='magnifier' /> + <Header.ToolBox.Action icon='key' /> + <Header.ToolBox.Action icon='kebab' /> </Header.ToolBox> </Header> ); }; -const toolboxRoom: IRoom = { - ...room, - msgs: 2, - u: { - _id: 'rocket.cat', - name: 'rocket.cat', - username: 'rocket.cat', - }, - usersCount: 2, -}; - -const renderWithBadge: ActionRenderer = (props) => ( - <Header.ToolBoxAction {...props}> - <Header.Badge variant='primary'>1</Header.Badge> - </Header.ToolBoxAction> -); - -const renderWithRedBadge: ActionRenderer = (props) => ( - <Header.ToolBoxAction {...props}> - <Header.Badge variant='danger'>2</Header.Badge> - </Header.ToolBoxAction> -); - -const renderWithOrangeBadge: ActionRenderer = (props) => ( - <Header.ToolBoxAction {...props}> - <Header.Badge variant='warning'>99</Header.Badge> - </Header.ToolBoxAction> -); - -addAction('render-action-example-badge', { - id: 'render-action-example-badge', - groups: ['channel'], - title: 'Phone', - icon: 'phone', - template: 'b', - order: 0, - renderAction: renderWithBadge, -}); - -addAction('render-action-example-badge-warning', { - id: 'render-action-example-badge-warning', - groups: ['channel'], - title: 'Threads', - icon: 'thread', - template: 'a', - order: 1, - renderAction: renderWithOrangeBadge, -}); - -addAction('render-action-example-badge-danger', { - id: 'render-action-example-badge-danger', - groups: ['channel'], - title: 'Discussion', - icon: 'discussion', - template: 'c', - order: 2, - renderAction: renderWithRedBadge, -}); - -export const WithToolboxContext: ComponentStory<typeof Header> = () => { - const icon = useRoomIcon(room); - const avatar = <RoomAvatar size='x40' room={room} />; +export const WithBurger = () => { return ( <Header> + <Header.ToolBox> + <Header.ToolBox.Action icon='burger' /> + </Header.ToolBox> <Header.Avatar>{avatar}</Header.Avatar> <Header.Content> <Header.Content.Row> @@ -183,17 +120,15 @@ export const WithToolboxContext: ComponentStory<typeof Header> = () => { </Header.Content.Row> </Header.Content> <Header.ToolBox> - <ToolboxProvider room={toolboxRoom}> - <ToolBox /> - </ToolboxProvider> + <Header.ToolBox.Action icon='magnifier' /> + <Header.ToolBox.Action icon='key' /> + <Header.ToolBox.Action icon='kebab' /> </Header.ToolBox> </Header> ); }; -export const Omnichannel = () => { - const icon = useRoomIcon(room); - const avatar = <RoomAvatar size='x40' room={room} />; +export const WithActionBadge = () => { return ( <Header> <Header.Avatar>{avatar}</Header.Avatar> @@ -202,17 +137,22 @@ export const Omnichannel = () => { {icon && <Header.Icon icon={icon} />} <Header.Title>{room.name}</Header.Title> <Header.State onClick={action('onClick')} icon='star' /> - <Header.State icon='key' /> - <Header.State icon='language' /> </Header.Content.Row> <Header.Content.Row> <Header.Subtitle>{room.name}</Header.Subtitle> </Header.Content.Row> </Header.Content> <Header.ToolBox> - <ToolboxProvider room={toolboxRoom}> - <ToolBox /> - </ToolboxProvider> + <Header.ToolBox.Action icon='phone'> + <Header.ToolBox.ActionBadge variant='primary'>1</Header.ToolBox.ActionBadge> + </Header.ToolBox.Action> + <Header.ToolBox.Action icon='phone'> + <Header.ToolBox.ActionBadge variant='danger'>2</Header.ToolBox.ActionBadge> + </Header.ToolBox.Action> + <Header.ToolBox.Action icon='phone'> + <Header.ToolBox.ActionBadge variant='warning'>99</Header.ToolBox.ActionBadge> + </Header.ToolBox.Action> + <Header.ToolBox.Action icon='kebab' /> </Header.ToolBox> </Header> ); diff --git a/apps/meteor/client/components/Header/Header.tsx b/packages/ui-client/src/components/Header/Header.tsx similarity index 82% rename from apps/meteor/client/components/Header/Header.tsx rename to packages/ui-client/src/components/Header/Header.tsx index c9c33e2ddaf..9939d5da895 100644 --- a/apps/meteor/client/components/Header/Header.tsx +++ b/packages/ui-client/src/components/Header/Header.tsx @@ -1,10 +1,11 @@ import { Box } from '@rocket.chat/fuselage'; import { useLayout } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; import HeaderDivider from './HeaderDivider'; -const Header: FC<any> = (props) => { +const Header: FC<ComponentProps<typeof Box>> = (props) => { const { isMobile } = useLayout(); return ( diff --git a/packages/ui-client/src/components/Header/HeaderAvatar.tsx b/packages/ui-client/src/components/Header/HeaderAvatar.tsx new file mode 100644 index 00000000000..6982752b903 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderAvatar.tsx @@ -0,0 +1,7 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; + +const HeaderAvatar: FC<ComponentProps<typeof Box>> = (props) => <Box mi='x4' display='flex' alignItems='center' {...props} />; + +export default HeaderAvatar; diff --git a/apps/meteor/client/components/Header/Content.tsx b/packages/ui-client/src/components/Header/HeaderContent.tsx similarity index 50% rename from apps/meteor/client/components/Header/Content.tsx rename to packages/ui-client/src/components/Header/HeaderContent.tsx index 91b0bc1cc14..0879fd17c74 100644 --- a/apps/meteor/client/components/Header/Content.tsx +++ b/packages/ui-client/src/components/Header/HeaderContent.tsx @@ -1,8 +1,9 @@ import { Box } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; -const Content: FC<any> = (props) => ( +const HeaderContent: FC<ComponentProps<typeof Box>> = (props) => ( <Box flexGrow={1} width={1} flexShrink={1} mi='x4' display='flex' justifyContent='center' flexDirection='column' {...props} /> ); -export default Content; +export default HeaderContent; diff --git a/packages/ui-client/src/components/Header/HeaderContentRow.tsx b/packages/ui-client/src/components/Header/HeaderContentRow.tsx new file mode 100644 index 00000000000..cb2517aa961 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderContentRow.tsx @@ -0,0 +1,9 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; + +const HeaderContentRow: FC<ComponentProps<typeof Box>> = (props) => ( + <Box alignItems='center' flexShrink={1} flexGrow={1} display='flex' {...props} /> +); + +export default HeaderContentRow; diff --git a/apps/meteor/client/components/Header/HeaderDivider.tsx b/packages/ui-client/src/components/Header/HeaderDivider.tsx similarity index 70% rename from apps/meteor/client/components/Header/HeaderDivider.tsx rename to packages/ui-client/src/components/Header/HeaderDivider.tsx index ae401221ab2..53b30dfcb6a 100644 --- a/apps/meteor/client/components/Header/HeaderDivider.tsx +++ b/packages/ui-client/src/components/Header/HeaderDivider.tsx @@ -1,5 +1,6 @@ import { Divider } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; +import type { FC } from 'react'; +import React from 'react'; const HeaderDivider: FC = () => <Divider mbs={-2} mbe={0} />; diff --git a/apps/meteor/client/components/Header/HeaderIcon.tsx b/packages/ui-client/src/components/Header/HeaderIcon.tsx similarity index 83% rename from apps/meteor/client/components/Header/HeaderIcon.tsx rename to packages/ui-client/src/components/Header/HeaderIcon.tsx index 89267c4ab0f..f4df61cb0c7 100644 --- a/apps/meteor/client/components/Header/HeaderIcon.tsx +++ b/packages/ui-client/src/components/Header/HeaderIcon.tsx @@ -1,5 +1,6 @@ import { Box, Icon } from '@rocket.chat/fuselage'; -import React, { FC, isValidElement, ReactElement } from 'react'; +import type { FC, ReactElement } from 'react'; +import React, { isValidElement } from 'react'; type HeaderIconProps = { icon: ReactElement | { name: string; color?: string } | null }; diff --git a/apps/meteor/client/components/Header/HeaderLink.tsx b/packages/ui-client/src/components/Header/HeaderLink.tsx similarity index 76% rename from apps/meteor/client/components/Header/HeaderLink.tsx rename to packages/ui-client/src/components/Header/HeaderLink.tsx index 1ed9e7723c7..ec11d5f750d 100644 --- a/apps/meteor/client/components/Header/HeaderLink.tsx +++ b/packages/ui-client/src/components/Header/HeaderLink.tsx @@ -1,7 +1,8 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; -import React, { ComponentProps, FC } from 'react'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; const HeaderLink: FC<ComponentProps<typeof Box>> = (props) => ( <Box diff --git a/packages/ui-client/src/components/Header/HeaderState.tsx b/packages/ui-client/src/components/Header/HeaderState.tsx new file mode 100644 index 00000000000..bb4b68cd9d5 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderState.tsx @@ -0,0 +1,7 @@ +import { Icon, IconButton } from '@rocket.chat/fuselage'; +import type { FC } from 'react'; +import React from 'react'; + +const HeaderState: FC<any> = (props) => (props.onClick ? <IconButton mini {...props} /> : <Icon size={16} name={props.icon} {...props} />); + +export default HeaderState; diff --git a/packages/ui-client/src/components/Header/HeaderSubtitle.tsx b/packages/ui-client/src/components/Header/HeaderSubtitle.tsx new file mode 100644 index 00000000000..221d0b58470 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderSubtitle.tsx @@ -0,0 +1,7 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; + +const HeaderSubtitle: FC<ComponentProps<typeof Box>> = (props) => <Box color='hint' fontScale='p2' withTruncatedText {...props} />; + +export default HeaderSubtitle; diff --git a/apps/meteor/client/components/Header/HeaderTag.tsx b/packages/ui-client/src/components/Header/HeaderTag.tsx similarity index 81% rename from apps/meteor/client/components/Header/HeaderTag.tsx rename to packages/ui-client/src/components/Header/HeaderTag.tsx index 567e86e629e..fdc6798c7d5 100644 --- a/apps/meteor/client/components/Header/HeaderTag.tsx +++ b/packages/ui-client/src/components/Header/HeaderTag.tsx @@ -1,5 +1,6 @@ import { Box, Tag } from '@rocket.chat/fuselage'; -import React, { ComponentProps, FC } from 'react'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; const HeaderTag: FC<ComponentProps<typeof Tag>> = ({ children, ...props }) => ( <Box display='flex' minWidth='65px' mi='x4'> diff --git a/apps/meteor/client/components/Header/HeaderTagIcon.tsx b/packages/ui-client/src/components/Header/HeaderTagIcon.tsx similarity index 71% rename from apps/meteor/client/components/Header/HeaderTagIcon.tsx rename to packages/ui-client/src/components/Header/HeaderTagIcon.tsx index 00609b23dfe..c92ca94bee8 100644 --- a/apps/meteor/client/components/Header/HeaderTagIcon.tsx +++ b/packages/ui-client/src/components/Header/HeaderTagIcon.tsx @@ -1,6 +1,7 @@ import { Box, Icon } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; -import React, { ComponentProps, FC, isValidElement, ReactElement } from 'react'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import type { ComponentProps, FC, ReactElement } from 'react'; +import React, { isValidElement } from 'react'; type HeaderIconProps = { icon: ReactElement | Pick<ComponentProps<typeof Icon>, 'name' | 'color'> | null; diff --git a/apps/meteor/client/components/Header/HeaderTagSkeleton.tsx b/packages/ui-client/src/components/Header/HeaderTagSkeleton.tsx similarity index 71% rename from apps/meteor/client/components/Header/HeaderTagSkeleton.tsx rename to packages/ui-client/src/components/Header/HeaderTagSkeleton.tsx index 9e0bb55e03f..b228d58e01b 100644 --- a/apps/meteor/client/components/Header/HeaderTagSkeleton.tsx +++ b/packages/ui-client/src/components/Header/HeaderTagSkeleton.tsx @@ -1,5 +1,6 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; +import type { FC } from 'react'; +import React from 'react'; const HeaderTagSkeleton: FC = () => <Skeleton width='x48' />; export default HeaderTagSkeleton; diff --git a/packages/ui-client/src/components/Header/HeaderTitle.tsx b/packages/ui-client/src/components/Header/HeaderTitle.tsx new file mode 100644 index 00000000000..7998940ab9f --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderTitle.tsx @@ -0,0 +1,9 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { FC, ComponentProps } from 'react'; +import React from 'react'; + +const HeaderTitle: FC<ComponentProps<typeof Box>> = (props) => ( + <Box color='default' mi='x4' is='h1' fontScale='h4' withTruncatedText {...props} /> +); + +export default HeaderTitle; diff --git a/packages/ui-client/src/components/Header/ToolBox/ToolBox.tsx b/packages/ui-client/src/components/Header/ToolBox/ToolBox.tsx new file mode 100644 index 00000000000..ade975433d3 --- /dev/null +++ b/packages/ui-client/src/components/Header/ToolBox/ToolBox.tsx @@ -0,0 +1,6 @@ +import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { FC, ComponentProps } from 'react'; + +const ToolBox: FC<ComponentProps<typeof ButtonGroup>> = (props) => <ButtonGroup role='toolbar' mi='x4' medium {...props} />; + +export default ToolBox; diff --git a/apps/meteor/client/components/Header/ToolBoxAction.tsx b/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx similarity index 85% rename from apps/meteor/client/components/Header/ToolBoxAction.tsx rename to packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx index 685e0913ee3..ef6eab7a347 100644 --- a/apps/meteor/client/components/Header/ToolBoxAction.tsx +++ b/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx @@ -1,11 +1,12 @@ import { IconButton } from '@rocket.chat/fuselage'; -import React, { FC } from 'react'; +import type { FC } from 'react'; +import React from 'react'; const ToolBoxAction: FC<any> = ({ id, icon, color, action, className, index, title, 'data-tooltip': tooltip, ...props }) => ( <IconButton data-qa-id={`ToolBoxAction-${icon}`} className={className} - onClick={action} + onClick={() => action(id)} data-toolbox={index} key={id} icon={icon} diff --git a/apps/meteor/client/components/Header/ToolBoxActionBadge.tsx b/packages/ui-client/src/components/Header/ToolBox/ToolBoxActionBadge.tsx similarity index 82% rename from apps/meteor/client/components/Header/ToolBoxActionBadge.tsx rename to packages/ui-client/src/components/Header/ToolBox/ToolBoxActionBadge.tsx index 39badd6f897..6a652421e91 100644 --- a/apps/meteor/client/components/Header/ToolBoxActionBadge.tsx +++ b/packages/ui-client/src/components/Header/ToolBox/ToolBoxActionBadge.tsx @@ -1,6 +1,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Badge } from '@rocket.chat/fuselage'; -import React, { ComponentProps, FC } from 'react'; +import type { ComponentProps, FC } from 'react'; +import React from 'react'; const ToolBoxActionBadge: FC<ComponentProps<typeof Badge>> = (props) => ( <Box diff --git a/packages/ui-client/src/components/Header/ToolBox/ToolboxDivider.tsx b/packages/ui-client/src/components/Header/ToolBox/ToolboxDivider.tsx new file mode 100644 index 00000000000..84b0d553a5d --- /dev/null +++ b/packages/ui-client/src/components/Header/ToolBox/ToolboxDivider.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { FC } from 'react'; + +const ToolboxDivider: FC<any> = () => ( + <Box is='hr' w='2px' display='block' backgroundColor='neutral-400' justifySelf='stretch' alignSelf='stretch' mi='x4' /> +); + +export default ToolboxDivider; diff --git a/packages/ui-client/src/components/Header/ToolBox/index.ts b/packages/ui-client/src/components/Header/ToolBox/index.ts new file mode 100644 index 00000000000..aec376204b7 --- /dev/null +++ b/packages/ui-client/src/components/Header/ToolBox/index.ts @@ -0,0 +1,10 @@ +import ToolBox from './ToolBox'; +import ToolBoxAction from './ToolBoxAction'; +import ToolBoxActionBadge from './ToolBoxActionBadge'; +import ToolboxDivider from './ToolboxDivider'; + +export default Object.assign(ToolBox, { + Action: ToolBoxAction, + ActionBadge: ToolBoxActionBadge, + Divider: ToolboxDivider, +}); diff --git a/apps/meteor/client/components/Header/index.ts b/packages/ui-client/src/components/Header/index.ts similarity index 50% rename from apps/meteor/client/components/Header/index.ts rename to packages/ui-client/src/components/Header/index.ts index 3efc558000d..431c24c25e1 100644 --- a/apps/meteor/client/components/Header/index.ts +++ b/packages/ui-client/src/components/Header/index.ts @@ -1,40 +1,35 @@ -import Avatar from './Avatar'; -import Button from './Button'; -import Content from './Content'; import Header from './Header'; +import HeaderAvatar from './HeaderAvatar'; +import HeaderContent from './HeaderContent'; +import HeaderContentRow from './HeaderContentRow'; import HeaderDivider from './HeaderDivider'; import HeaderIcon from './HeaderIcon'; import HeaderLink from './HeaderLink'; +import HeaderState from './HeaderState'; +import HeaderSubtitle from './HeaderSubtitle'; import HeaderTag from './HeaderTag'; import HeaderTagIcon from './HeaderTagIcon'; import HeaderTagSkeleton from './HeaderTagSkeleton'; -import Row from './Row'; -import State from './State'; -import Subtitle from './Subtitle'; -import Title from './Title'; +import HeaderTitle from './HeaderTitle'; import ToolBox from './ToolBox'; -import ToolBoxAction from './ToolBoxAction'; -import ToolBoxActionBadge from './ToolBoxActionBadge'; export default Object.assign(Header, { - Button, - State, - Avatar, - Content: Object.assign(Content, { - Row, - }), - Title, - Subtitle, - ToolBox, - ToolBoxAction: Object.assign(ToolBoxAction, { - Badge: ToolBoxActionBadge, + State: HeaderState, + Avatar: HeaderAvatar, + Content: Object.assign(HeaderContent, { + Row: HeaderContentRow, }), + Title: HeaderTitle, + Subtitle: HeaderSubtitle, Divider: HeaderDivider, Icon: HeaderIcon, Link: HeaderLink, + ToolBox: Object.assign(ToolBox, { + Action: ToolBox.Action, + ActionBadge: ToolBox.ActionBadge, + }), Tag: Object.assign(HeaderTag, { Icon: HeaderTagIcon, Skeleton: HeaderTagSkeleton, }), - Badge: ToolBoxActionBadge, }); diff --git a/packages/ui-client/src/components/UserStatus/UserStatus.stories.tsx b/packages/ui-client/src/components/UserStatus/UserStatus.stories.tsx index dba6ac99ab5..9bd050252c3 100644 --- a/packages/ui-client/src/components/UserStatus/UserStatus.stories.tsx +++ b/packages/ui-client/src/components/UserStatus/UserStatus.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import * as UserStatus from '.'; -import { useAutoSequence } from '../../stories/hooks/useAutoSequence'; +import { useAutoSequence } from '../../hooks/useAutoSequence'; export default { title: 'Components/UserStatus', diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 1d7fab984ac..2b54d1a69e9 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -4,3 +4,4 @@ export { default as TextSeparator } from './TextSeparator'; export * from './TooltipComponent'; export * as UserStatus from './UserStatus'; export { default as Card } from './Card'; +export { default as Header } from './Header'; diff --git a/packages/ui-client/src/stories/hooks/useAutoSequence.ts b/packages/ui-client/src/hooks/useAutoSequence.ts similarity index 100% rename from packages/ui-client/src/stories/hooks/useAutoSequence.ts rename to packages/ui-client/src/hooks/useAutoSequence.ts -- GitLab From 8dc8817d9ed41598bed5be79b917795066b7569b Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Thu, 15 Sep 2022 15:45:18 -0300 Subject: [PATCH 040/107] [IMPROVE] Changed dial pad appearance to match original design (#26863) --- .../voip/modal/DialPad/DialPadModal.tsx | 16 +++++++--- .../ee/client/voip/modal/DialPad/Pad.tsx | 2 +- .../client/voip/modal/DialPad/PadButton.tsx | 31 ++++++++++++++----- .../voip/modal/DialPad/hooks/useDialPad.tsx | 24 +++++--------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx index 501f3ff9b97..ae8227a4efd 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/DialPadModal.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { Field, Modal, IconButton } from '@rocket.chat/fuselage'; import React, { ReactElement } from 'react'; @@ -12,7 +13,13 @@ type DialPadModalProps = { handleClose: () => void; }; -const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalProps): ReactElement => { +const callButtonStyle = css` + > i { + font-size: 32px !important; + } +`; + +const DialPadModal = ({ initialValue, errorMessage: initialErrorMessage, handleClose }: DialPadModalProps): ReactElement => { const { inputName, inputRef, @@ -23,12 +30,12 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP handlePadButtonClick, handlePadButtonLongPressed, handleCallButtonClick, - } = useDialPad({ initialValue, errorMessage }); + } = useDialPad({ initialValue, initialErrorMessage }); useEnterKey(handleCallButtonClick, isButtonDisabled); return ( - <Modal maxWidth='400px'> + <Modal width='432px'> <Modal.Header> <Modal.Title /> <Modal.Close onClick={handleClose} /> @@ -43,7 +50,7 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP isButtonDisabled={isButtonDisabled} handleOnChange={handleOnChange} /> - <Field.Error h='20px' textAlign='center'> + <Field.Error fontSize='12px' h='16px' textAlign='center'> {inputError} </Field.Error> </Field> @@ -51,6 +58,7 @@ const DialPadModal = ({ initialValue, errorMessage, handleClose }: DialPadModalP </Modal.Content> <Modal.Footer justifyContent='center'> <IconButton + className={callButtonStyle} icon='phone' disabled={isButtonDisabled} borderRadius='full' diff --git a/apps/meteor/ee/client/voip/modal/DialPad/Pad.tsx b/apps/meteor/ee/client/voip/modal/DialPad/Pad.tsx index e6b071092a7..1040d998aa0 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/Pad.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/Pad.tsx @@ -27,7 +27,7 @@ const Pad = ({ onClickPadButton: (digit: PadDigit[0]) => void; onLongPressPadButton: (digit: PadDigit[1]) => void; }): ReactElement => ( - <Box display='flex' flexWrap='wrap' mi='-8px' mbs='28px'> + <Box display='flex' flexWrap='wrap' justifyContent='center' mi='-8px' mbs='24px'> {digits.map((digit, idx) => ( <PadButton key={idx} onClickPadButton={onClickPadButton} onLongPressPadButton={onLongPressPadButton}> {digit} diff --git a/apps/meteor/ee/client/voip/modal/DialPad/PadButton.tsx b/apps/meteor/ee/client/voip/modal/DialPad/PadButton.tsx index 98a292e852a..b431a81163a 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/PadButton.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/PadButton.tsx @@ -1,9 +1,27 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box, Button } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors'; import React, { ReactElement } from 'react'; import type { PadDigit } from './Pad'; import { useLongPress } from './hooks/useLongPress'; +const padButtonStyle = css` + background-color: transparent; + width: 94px; + height: 64px; + padding: 8px; + margin: 8px; + border: none; + display: flex; + flex-direction: column; + align-items: center; + + &:hover { + background-color: ${colors.n400}; + } +`; + const PadButton = ({ children, onClickPadButton, @@ -20,20 +38,17 @@ const PadButton = ({ return ( <Button - m='8px' - pb='8px' - minWidth='28%' - display='flex' - flexDirection='column' - alignItems='center' + className={padButtonStyle} onClick={onClick} onMouseDown={onMouseDown} onMouseUp={onMouseUp} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - <Box fontSize='h2'>{firstDigit}</Box> - <Box fontSize='c1' color='info'> + <Box is='span' fontSize='h2' lineHeight='32px'> + {firstDigit} + </Box> + <Box is='span' fontSize='c1' lineHeight='16px' color='info'> {secondDigit} </Box> </Button> diff --git a/apps/meteor/ee/client/voip/modal/DialPad/hooks/useDialPad.tsx b/apps/meteor/ee/client/voip/modal/DialPad/hooks/useDialPad.tsx index e4dd8af99c4..79206003a93 100644 --- a/apps/meteor/ee/client/voip/modal/DialPad/hooks/useDialPad.tsx +++ b/apps/meteor/ee/client/voip/modal/DialPad/hooks/useDialPad.tsx @@ -20,10 +20,10 @@ type DialPadStateHandlers = { type DialPadProps = { initialValue?: string; - errorMessage?: string; + initialErrorMessage?: string; }; -export const useDialPad = ({ initialValue, errorMessage }: DialPadProps): DialPadStateHandlers => { +export const useDialPad = ({ initialValue, initialErrorMessage }: DialPadProps): DialPadStateHandlers => { const t = useTranslation(); const outboundClient = useOutboundDialer(); const { closeDialModal } = useDialModal(); @@ -35,7 +35,7 @@ export const useDialPad = ({ initialValue, errorMessage }: DialPadProps): DialPa setError, clearErrors, watch, - formState: { errors }, + formState: { errors, isDirty }, } = useForm<{ PhoneInput: string }>({ defaultValues: { PhoneInput: initialValue, @@ -50,14 +50,14 @@ export const useDialPad = ({ initialValue, errorMessage }: DialPadProps): DialPa const handleBackspaceClick = useCallback((): void => { clearErrors(); - setValue('PhoneInput', value.slice(0, -1)); + setValue('PhoneInput', value.slice(0, -1), { shouldDirty: true }); }, [clearErrors, setValue, value]); const handlePadButtonClick = useCallback( (digit: PadDigit[0]): void => { clearErrors(); - setValue('PhoneInput', value + digit); + setValue('PhoneInput', value + digit, { shouldDirty: true }); }, [clearErrors, setValue, value], ); @@ -82,17 +82,7 @@ export const useDialPad = ({ initialValue, errorMessage }: DialPadProps): DialPa closeDialModal(); }, [outboundClient, setError, t, value, closeDialModal]); - const handleOnChange = useCallback( - (e) => { - clearErrors(); - onChange(e); - }, - [clearErrors, onChange], - ); - - useEffect(() => { - setError('PhoneInput', { message: errorMessage }); - }, [setError, errorMessage]); + const handleOnChange = useCallback((e) => onChange(e), [onChange]); useEffect(() => { setDisabled(!value); @@ -105,7 +95,7 @@ export const useDialPad = ({ initialValue, errorMessage }: DialPadProps): DialPa return { inputName: 'PhoneInput', inputRef: ref, - inputError: errors.PhoneInput?.message, + inputError: isDirty ? errors.PhoneInput?.message : initialErrorMessage, isButtonDisabled: disabled, handleOnChange, handleBackspaceClick, -- GitLab From 6d3b20d81c230784ccace7f775f2249a2583faa7 Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Thu, 15 Sep 2022 17:34:52 -0300 Subject: [PATCH 041/107] Chore: Move presence to package (#25541) --- apps/meteor/app/api/server/v1/users.ts | 8 +- apps/meteor/app/apps/server/bridges/users.ts | 13 +- .../app/apps/server/{index.js => index.ts} | 1 - apps/meteor/app/apps/server/status.ts | 16 - apps/meteor/app/lib/server/functions/index.ts | 2 +- .../app/lib/server/functions/setStatusText.ts | 49 +- .../meteor/app/livechat/server/agentStatus.js | 10 - apps/meteor/app/livechat/server/index.js | 2 - .../app/livechat/server/visitorStatus.js | 12 - apps/meteor/app/models/server/models/Users.js | 10 - apps/meteor/app/voip/server/startup.ts | 20 +- apps/meteor/ee/server/NetworkBroker.ts | 14 +- .../ee/server/services/ecosystem.config.js | 12 +- apps/meteor/ee/server/services/package.json | 5 +- .../ee/server/services/presence/Presence.ts | 56 - .../presence/actions/newConnection.ts | 49 - .../presence/actions/removeConnection.ts | 23 - .../presence/actions/removeLostConnections.ts | 69 - .../services/presence/actions/setStatus.ts | 63 - .../presence/actions/updateUserPresence.ts | 42 - .../ee/server/services/presence/service.ts | 6 - .../startup/server/{index.js => index.ts} | 1 - .../users-presence/server/activeUsers.js | 36 - .../imports/users-presence/server/index.js | 1 - apps/meteor/package.json | 5 +- .../server/database/watchCollections.ts | 2 - apps/meteor/server/main.ts | 1 + apps/meteor/server/methods/userPresence.ts | 28 + apps/meteor/server/models/raw/Users.js | 29 + .../meteor/server/models/raw/UsersSessions.ts | 104 +- .../modules/watchers/watchers.module.ts | 22 - apps/meteor/server/sdk/api.ts | 6 + apps/meteor/server/sdk/lib/Api.ts | 23 +- apps/meteor/server/sdk/lib/Events.ts | 8 +- apps/meteor/server/sdk/lib/LocalBroker.ts | 15 +- apps/meteor/server/sdk/types/IApiService.ts | 27 + .../server/sdk/types/IAppsEngineService.ts | 3 + apps/meteor/server/sdk/types/IBroker.ts | 12 +- .../server/sdk/types/IOmnichannelService.ts | 3 + apps/meteor/server/sdk/types/IPresence.ts | 13 +- apps/meteor/server/sdk/types/ServiceClass.ts | 31 +- .../server/services/apps-engine/service.ts | 17 + apps/meteor/server/services/meteor/service.ts | 32 +- .../server/services/omnichannel/service.ts | 20 + apps/meteor/server/services/startup.ts | 7 + apps/meteor/server/startup/index.ts | 10 +- apps/meteor/server/startup/presence.js | 19 - apps/meteor/server/startup/presence.ts | 35 + .../server/startup/presenceTroubleshoot.ts | 11 + apps/meteor/server/startup/watchDb.ts | 11 +- apps/meteor/server/stream/streamBroadcast.js | 14 - docker-compose-ci.yml | 4 +- ee/apps/ddp-streamer/package.json | 2 + ee/apps/ddp-streamer/src/DDPStreamer.ts | 38 +- ee/apps/ddp-streamer/src/configureServer.ts | 35 +- ee/apps/presence-service/.eslintrc | 16 + ee/apps/presence-service/Dockerfile | 38 + ee/apps/presence-service/package.json | 43 + ee/apps/presence-service/src/service.ts | 28 + ee/apps/presence-service/tsconfig.json | 31 + .../core-typings/src/ISocketConnection.ts | 1 + .../model-typings/src/models/IUsersModel.ts | 12 +- .../src/models/IUsersSessionsModel.ts | 13 +- packages/models/src/proxify.ts | 5 +- packages/presence/.eslintrc | 12 + packages/presence/.gitignore | 1 + packages/presence/babel.config.js | 3 + packages/presence/package.json | 37 + packages/presence/src/Presence.ts | 189 ++ .../src}/lib/processConnectionStatus.ts | 0 .../tests/lib/processConnectionStatus.test.ts | 92 +- packages/presence/tsconfig.json | 30 + packages/rest-typings/src/v1/users.ts | 4 +- turbo.json | 3 + yarn.lock | 1578 ++++++++++++++++- 75 files changed, 2553 insertions(+), 690 deletions(-) rename apps/meteor/app/apps/server/{index.js => index.ts} (75%) delete mode 100644 apps/meteor/app/apps/server/status.ts delete mode 100644 apps/meteor/app/livechat/server/agentStatus.js delete mode 100644 apps/meteor/app/livechat/server/visitorStatus.js delete mode 100755 apps/meteor/ee/server/services/presence/Presence.ts delete mode 100755 apps/meteor/ee/server/services/presence/actions/newConnection.ts delete mode 100755 apps/meteor/ee/server/services/presence/actions/removeConnection.ts delete mode 100755 apps/meteor/ee/server/services/presence/actions/removeLostConnections.ts delete mode 100755 apps/meteor/ee/server/services/presence/actions/setStatus.ts delete mode 100755 apps/meteor/ee/server/services/presence/actions/updateUserPresence.ts delete mode 100755 apps/meteor/ee/server/services/presence/service.ts rename apps/meteor/imports/startup/server/{index.js => index.ts} (70%) delete mode 100644 apps/meteor/imports/users-presence/server/activeUsers.js delete mode 100644 apps/meteor/imports/users-presence/server/index.js create mode 100644 apps/meteor/server/methods/userPresence.ts create mode 100644 apps/meteor/server/sdk/types/IApiService.ts create mode 100644 apps/meteor/server/sdk/types/IAppsEngineService.ts create mode 100644 apps/meteor/server/sdk/types/IOmnichannelService.ts create mode 100644 apps/meteor/server/services/apps-engine/service.ts create mode 100644 apps/meteor/server/services/omnichannel/service.ts delete mode 100644 apps/meteor/server/startup/presence.js create mode 100644 apps/meteor/server/startup/presence.ts create mode 100644 apps/meteor/server/startup/presenceTroubleshoot.ts create mode 100644 ee/apps/presence-service/.eslintrc create mode 100644 ee/apps/presence-service/Dockerfile create mode 100644 ee/apps/presence-service/package.json create mode 100755 ee/apps/presence-service/src/service.ts create mode 100644 ee/apps/presence-service/tsconfig.json create mode 100644 packages/presence/.eslintrc create mode 100644 packages/presence/.gitignore create mode 100644 packages/presence/babel.config.js create mode 100644 packages/presence/package.json create mode 100755 packages/presence/src/Presence.ts rename {apps/meteor/ee/server/services/presence => packages/presence/src}/lib/processConnectionStatus.ts (100%) rename apps/meteor/ee/tests/unit/server/services/presence/lib/processConnectionStatus.tests.ts => packages/presence/tests/lib/processConnectionStatus.test.ts (58%) create mode 100644 packages/presence/tsconfig.json diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 2d902ac1ce1..858881ae7f7 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -39,9 +39,9 @@ import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKe import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { Team } from '../../../../server/sdk'; import { isValidQuery } from '../lib/isValidQuery'; -import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; import { getURL } from '../../../utils/server'; import { getUploadFormData } from '../lib/getUploadFormData'; +import { api } from '../../../../server/sdk/api'; API.v1.addRoute( 'users.getAvatar', @@ -1031,7 +1031,11 @@ API.v1.addRoute( }, }); - setUserStatus(user, status); + const { _id, username, statusText, roles, name } = user; + api.broadcast('presence.status', { + user: { status, _id, username, statusText, roles, name }, + previousStatus: user.status, + }); } else { throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { method: 'users.setStatus', diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index af00a8be2cd..b239ab7866f 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,5 +1,4 @@ import { Random } from 'meteor/random'; -import { UserPresence } from 'meteor/konecty:user-presence'; import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; import type { IUserCreationOptions, IUser } from '@rocket.chat/apps-engine/definition/users'; import { Subscriptions, Users as UsersRaw } from '@rocket.chat/models'; @@ -84,7 +83,11 @@ export class AppUserBridge extends UserBridge { return true; } - protected async update(user: IUser & { id: string }, fields: Partial<IUser>, appId: string): Promise<boolean> { + protected async update( + user: IUser & { id: string }, + fields: Partial<IUser> & { statusDefault: string }, + appId: string, + ): Promise<boolean> { this.orch.debugLog(`The App ${appId} is updating a user`); if (!user) { @@ -98,12 +101,12 @@ export class AppUserBridge extends UserBridge { const { status } = fields; delete fields.status; - await UsersRaw.update({ _id: user.id }, { $set: fields as any }); - if (status) { - UserPresence.setDefaultStatus(user.id, status); + fields.statusDefault = status; } + await UsersRaw.updateOne({ _id: user.id }, { $set: fields as any }); + return true; } diff --git a/apps/meteor/app/apps/server/index.js b/apps/meteor/app/apps/server/index.ts similarity index 75% rename from apps/meteor/app/apps/server/index.js rename to apps/meteor/app/apps/server/index.ts index 753a21c4fba..ad3096af315 100644 --- a/apps/meteor/app/apps/server/index.js +++ b/apps/meteor/app/apps/server/index.ts @@ -1,4 +1,3 @@ import './cron'; -import './status.ts'; export { Apps, AppEvents } from './orchestrator'; diff --git a/apps/meteor/app/apps/server/status.ts b/apps/meteor/app/apps/server/status.ts deleted file mode 100644 index 3dd020df92e..00000000000 --- a/apps/meteor/app/apps/server/status.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { UserPresenceMonitor } from 'meteor/konecty:user-presence'; - -import { AppEvents, Apps } from './orchestrator'; - -UserPresenceMonitor.onSetUserStatus((...args: any) => { - const [user, status] = args; - - // App IPostUserStatusChanged event hook - Promise.await( - Apps.triggerEvent(AppEvents.IPostUserStatusChanged, { - user, - currentStatus: status, - previousStatus: user.status, - }), - ); -}); diff --git a/apps/meteor/app/lib/server/functions/index.ts b/apps/meteor/app/lib/server/functions/index.ts index 28b32d26b20..614cc8ac0b0 100644 --- a/apps/meteor/app/lib/server/functions/index.ts +++ b/apps/meteor/app/lib/server/functions/index.ts @@ -26,7 +26,7 @@ export { saveUserIdentity } from './saveUserIdentity'; export { sendMessage } from './sendMessage'; export { setEmail } from './setEmail'; export { setRealName, _setRealName } from './setRealName'; -export { setStatusText, _setStatusText, _setStatusTextPromise } from './setStatusText'; +export { setStatusText } from './setStatusText'; export { getStatusText } from './getStatusText'; export { setUserAvatar } from './setUserAvatar'; export { _setUsername, setUsername } from './setUsername'; diff --git a/apps/meteor/app/lib/server/functions/setStatusText.ts b/apps/meteor/app/lib/server/functions/setStatusText.ts index 3077061f48b..048d4c25353 100644 --- a/apps/meteor/app/lib/server/functions/setStatusText.ts +++ b/apps/meteor/app/lib/server/functions/setStatusText.ts @@ -1,21 +1,22 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import type { IUser } from '@rocket.chat/core-typings'; -import { Users as UsersRaw } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; -import { Users } from '../../../models/server'; import { hasPermission } from '../../../authorization/server'; import { RateLimiter } from '../lib'; import { api } from '../../../../server/sdk/api'; -export const _setStatusTextPromise = async function (userId: string, statusText: string): Promise<boolean> { +async function _setStatusTextPromise(userId: string, statusText: string): Promise<boolean> { if (!userId) { return false; } statusText = s.trim(statusText).substr(0, 120); - const user = await UsersRaw.findOneById(userId); + const user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'roles' | 'statusText'>>(userId, { + projection: { username: 1, name: 1, status: 1, roles: 1, statusText: 1 }, + }); if (!user) { return false; @@ -25,44 +26,20 @@ export const _setStatusTextPromise = async function (userId: string, statusText: return true; } - await UsersRaw.updateStatusText(user._id, statusText); + await Users.updateStatusText(user._id, statusText); - const { _id, username, status } = user; + const { _id, username, status, name, roles } = user; api.broadcast('presence.status', { - user: { _id, username, status, statusText }, + user: { _id, username, status, statusText, name, roles }, + previousStatus: status, }); return true; -}; - -export const _setStatusText = function (userId: any, statusText: string): IUser | boolean { - statusText = s.trim(statusText); - if (statusText.length > 120) { - statusText = statusText.substr(0, 120); - } - - if (!userId) { - return false; - } - - const user = Users.findOneById(userId); +} - // User already has desired statusText, return - if (user.statusText === statusText) { - return user; - } - - // Set new statusText - Users.updateStatusText(user._id, statusText); - user.statusText = statusText; - - const { _id, username, status } = user; - api.broadcast('presence.status', { - user: { _id, username, status, statusText }, - }); - - return true; -}; +function _setStatusText(userId: any, statusText: string): boolean { + return Promise.await(_setStatusTextPromise(userId, statusText)); +} export const setStatusText = RateLimiter.limitFunction(_setStatusText, 5, 60000, { 0() { diff --git a/apps/meteor/app/livechat/server/agentStatus.js b/apps/meteor/app/livechat/server/agentStatus.js deleted file mode 100644 index a15d67f90eb..00000000000 --- a/apps/meteor/app/livechat/server/agentStatus.js +++ /dev/null @@ -1,10 +0,0 @@ -import { UserPresenceMonitor } from 'meteor/konecty:user-presence'; - -import { Livechat } from './lib/Livechat'; -import { hasAnyRole } from '../../authorization/server/functions/hasRole'; - -UserPresenceMonitor.onSetUserStatus((user, status) => { - if (hasAnyRole(user._id, ['livechat-manager', 'livechat-monitor', 'livechat-agent'])) { - Livechat.notifyAgentStatusChanged(user._id, status); - } -}); diff --git a/apps/meteor/app/livechat/server/index.js b/apps/meteor/app/livechat/server/index.js index c44390cd6de..7ba865237ba 100644 --- a/apps/meteor/app/livechat/server/index.js +++ b/apps/meteor/app/livechat/server/index.js @@ -1,8 +1,6 @@ import './livechat'; import './config'; import './startup'; -import './visitorStatus'; -import './agentStatus'; import '../lib/messageTypes'; import './hooks/beforeCloseRoom'; import './hooks/beforeDelegateAgent'; diff --git a/apps/meteor/app/livechat/server/visitorStatus.js b/apps/meteor/app/livechat/server/visitorStatus.js deleted file mode 100644 index 466d82c8efc..00000000000 --- a/apps/meteor/app/livechat/server/visitorStatus.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { UserPresenceEvents } from 'meteor/konecty:user-presence'; - -import { Livechat } from './lib/Livechat'; - -Meteor.startup(() => { - UserPresenceEvents.on('setStatus', (session, status, metadata) => { - if (metadata && metadata.visitor) { - Livechat.notifyGuestStatusChanged(metadata.visitor, status); - } - }); -}); diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index a54a201e79b..e94a2458104 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -1082,16 +1082,6 @@ export class Users extends Base { return this.update(_id, update); } - updateStatusById(_id, status) { - const update = { - $set: { - status, - }, - }; - - return this.update(_id, update); - } - addPasswordToHistory(_id, password) { const update = { $push: { diff --git a/apps/meteor/app/voip/server/startup.ts b/apps/meteor/app/voip/server/startup.ts index 7a0dc60f188..e57fa27f0dc 100644 --- a/apps/meteor/app/voip/server/startup.ts +++ b/apps/meteor/app/voip/server/startup.ts @@ -1,17 +1,29 @@ import { settings } from '../../settings/server'; import { Voip } from '../../../server/sdk'; -settings.watch('VoIP_Enabled', (value: boolean) => { - return value ? Voip.init() : Voip.stop(); +settings.watch('VoIP_Enabled', async function (value: boolean) { + try { + if (value) { + await Voip.init(); + } else { + await Voip.stop(); + } + } catch (e) { + // do nothing + } }); settings.changeMultiple( ['VoIP_Management_Server_Host', 'VoIP_Management_Server_Port', 'VoIP_Management_Server_Username', 'VoIP_Management_Server_Password'], - (_values) => { + async function (_values) { // Here, if 4 settings are changed at once, we're getting 4 diff callbacks. The good part is that all callbacks are fired almost instantly // So to avoid stopping/starting voip too often, we debounce the call and restart 1 second after the last setting has reached us. if (settings.get('VoIP_Enabled')) { - Voip.refresh(); + try { + await Voip.refresh(); + } catch (e) { + // do nothing + } } }, ); diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/apps/meteor/ee/server/NetworkBroker.ts index 3083613183b..cb6d9c3cf55 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/apps/meteor/ee/server/NetworkBroker.ts @@ -2,7 +2,7 @@ import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; import { asyncLocalStorage } from '../../server/sdk'; import type { IBroker, IBrokerNode, IServiceMetrics } from '../../server/sdk/types/IBroker'; -import type { ServiceClass } from '../../server/sdk/types/ServiceClass'; +import type { IServiceClass } from '../../server/sdk/types/ServiceClass'; import type { EventSignatures } from '../../server/sdk/lib/Events'; const events: { [k: string]: string } = { @@ -35,6 +35,7 @@ export class NetworkBroker implements IBroker { this.metrics = broker.metrics; + // TODO move this to a proper startup method? this.started = this.broker.start(); } @@ -73,18 +74,19 @@ export class NetworkBroker implements IBroker { return this.broker.call(method, data); } - destroyService(instance: ServiceClass): void { + destroyService(instance: IServiceClass): void { this.broker.destroyService(instance.getName()); } - createService(instance: ServiceClass): void { + createService(instance: IServiceClass): void { const methods = ( instance.constructor?.name === 'Object' ? Object.getOwnPropertyNames(instance) : Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) ).filter((name) => name !== 'constructor'); - if (!instance.getEvents() || !methods.length) { + const instanceEvents = instance.getEvents(); + if (!instanceEvents && !methods.length) { return; } @@ -98,7 +100,7 @@ export class NetworkBroker implements IBroker { this.broker.logger.debug({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID }); return; } - this.broker.logger.info({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID }); + this.broker.logger.warn({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID }); this.destroyService(instance); }); } @@ -109,7 +111,7 @@ export class NetworkBroker implements IBroker { name, actions: {}, ...dependencies, - events: instance.getEvents().reduce<Record<string, (ctx: Context) => void>>((map, eventName) => { + events: instanceEvents.reduce<Record<string, (ctx: Context) => void>>((map, eventName) => { map[eventName] = /^\$/.test(eventName) ? (ctx: Context): void => { // internal events params are not an array diff --git a/apps/meteor/ee/server/services/ecosystem.config.js b/apps/meteor/ee/server/services/ecosystem.config.js index 26e81a9178e..1b4190cebb4 100644 --- a/apps/meteor/ee/server/services/ecosystem.config.js +++ b/apps/meteor/ee/server/services/ecosystem.config.js @@ -6,18 +6,18 @@ module.exports = { name: 'authorization', watch: [...watch, '../../../server/services/authorization'], }, - { - name: 'presence', - }, + // { + // name: 'presence', + // }, { name: 'account', }, { name: 'stream-hub', }, - { - name: 'ddp-streamer', - }, + // { + // name: 'ddp-streamer', + // }, ].map((app) => Object.assign(app, { script: app.script || `ts-node --files ${app.name}/service.ts`, diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index f22ecc5cc62..78acd594366 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -7,9 +7,9 @@ "scripts": { "dev": "pm2 start ecosystem.config.js", "pm2": "pm2", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} run-p start:account start:authorization start:stream-hub", "start:account": "ts-node --files ./account/service.ts", "start:authorization": "ts-node --files ./authorization/service.ts", - "start:presence": "ts-node --files ./presence/service.ts", "start:stream-hub": "ts-node --files ./stream-hub/service.ts", "typecheck": "tsc --noEmit --skipLibCheck", "build": "tsc", @@ -62,9 +62,10 @@ "@types/fibers": "^3.1.1", "@types/node": "^14.18.21", "@types/ws": "^8.5.3", + "npm-run-all": "^4.1.5", "pino-pretty": "^7.6.1", "pm2": "^5.2.0", - "ts-node": "^10.8.1", + "ts-node": "^10.9.1", "typescript": "~4.5.5" }, "volta": { diff --git a/apps/meteor/ee/server/services/presence/Presence.ts b/apps/meteor/ee/server/services/presence/Presence.ts deleted file mode 100755 index b84625bbbb1..00000000000 --- a/apps/meteor/ee/server/services/presence/Presence.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { UserStatus } from '@rocket.chat/core-typings'; - -import { newConnection } from './actions/newConnection'; -import { removeConnection } from './actions/removeConnection'; -import { removeLostConnections } from './actions/removeLostConnections'; -import { setStatus, setConnectionStatus } from './actions/setStatus'; -import { updateUserPresence } from './actions/updateUserPresence'; -import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; -import type { IPresence } from '../../../../server/sdk/types/IPresence'; -import type { IBrokerNode } from '../../../../server/sdk/types/IBroker'; - -export class Presence extends ServiceClass implements IPresence { - protected name = 'presence'; - - async onNodeDisconnected({ node }: { node: IBrokerNode }): Promise<void> { - const affectedUsers = await this.removeLostConnections(node.id); - return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); - } - - async started(): Promise<void> { - setTimeout(async () => { - const affectedUsers = await this.removeLostConnections(); - return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); - }, 100); - } - - async newConnection(uid: string, session: string): Promise<{ uid: string; connectionId: string } | undefined> { - const result = await newConnection(uid, session, this.context); - await updateUserPresence(uid); - return result; - } - - async removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }> { - const result = await removeConnection(uid, session); - await updateUserPresence(uid); - return result; - } - - async removeLostConnections(nodeID?: string): Promise<string[]> { - return removeLostConnections(nodeID, this.context); - } - - async setStatus(uid: string, status: UserStatus, statusText?: string): Promise<boolean> { - return setStatus(uid, status, statusText); - } - - async setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> { - const result = await setConnectionStatus(uid, status, session); - await updateUserPresence(uid); - return result; - } - - async updateUserPresence(uid: string): Promise<void> { - return updateUserPresence(uid); - } -} diff --git a/apps/meteor/ee/server/services/presence/actions/newConnection.ts b/apps/meteor/ee/server/services/presence/actions/newConnection.ts deleted file mode 100755 index 241f7187e2c..00000000000 --- a/apps/meteor/ee/server/services/presence/actions/newConnection.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { getCollection, Collections } from '../../mongo'; -import type { IServiceContext } from '../../../../../server/sdk/types/ServiceClass'; - -const status = 'online'; - -export async function newConnection( - uid: string, - session: string, - context?: IServiceContext, -): Promise<{ uid: string; connectionId: string } | undefined> { - const instanceId = context?.nodeID; - - if (!instanceId) { - return; - } - - const query = { - _id: uid, - }; - - const now = new Date(); - - const update = { - $push: { - connections: { - id: session, - instanceId, - status, - _createdAt: now, - _updatedAt: now, - }, - }, - }; - - // if (metadata) { - // update.$set = { - // metadata: metadata - // }; - // connection.metadata = metadata; - // } - - const UserSession = await getCollection(Collections.UserSession); - await UserSession.updateOne(query, update, { upsert: true }); - - return { - uid, - connectionId: session, - }; -} diff --git a/apps/meteor/ee/server/services/presence/actions/removeConnection.ts b/apps/meteor/ee/server/services/presence/actions/removeConnection.ts deleted file mode 100755 index f4cca658171..00000000000 --- a/apps/meteor/ee/server/services/presence/actions/removeConnection.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getCollection, Collections } from '../../mongo'; - -export async function removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }> { - const query = { - 'connections.id': session, - }; - - const update = { - $pull: { - connections: { - id: session, - }, - }, - }; - - const UserSession = await getCollection(Collections.UserSession); - await UserSession.updateMany(query, update); - - return { - uid, - session, - }; -} diff --git a/apps/meteor/ee/server/services/presence/actions/removeLostConnections.ts b/apps/meteor/ee/server/services/presence/actions/removeLostConnections.ts deleted file mode 100755 index d2b54d805bf..00000000000 --- a/apps/meteor/ee/server/services/presence/actions/removeLostConnections.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { Collection } from 'mongodb'; -import type { IUserSession } from '@rocket.chat/core-typings'; - -import { getCollection, Collections } from '../../mongo'; -import type { IServiceContext } from '../../../../../server/sdk/types/ServiceClass'; - -async function getAffectedUsers(model: Collection<IUserSession>, query: object): Promise<string[]> { - const list = await model.find<{ _id: string }>(query, { projection: { _id: 1 } }).toArray(); - return list.map(({ _id }) => _id); -} - -// TODO: Change this to use find and modify -export async function removeLostConnections(nodeID?: string, context?: IServiceContext): Promise<string[]> { - const UserSession = await getCollection<IUserSession>(Collections.UserSession); - - if (nodeID) { - const query = { - 'connections.instanceId': nodeID, - }; - const update = { - $pull: { - connections: { - instanceId: nodeID, - }, - }, - }; - const affectedUsers = await getAffectedUsers(UserSession, query); - - const { modifiedCount } = await UserSession.updateMany(query, update); - - if (modifiedCount === 0) { - return []; - } - - return affectedUsers; - } - - if (!context) { - return []; - } - - const nodes = await context.broker.nodeList(); - - const ids = nodes.filter((node) => node.available).map(({ id }) => id); - - const affectedUsers = await getAffectedUsers(UserSession, { - 'connections.instanceId': { - $exists: true, - $nin: ids, - }, - }); - - const update = { - $pull: { - connections: { - instanceId: { - $nin: ids, - }, - }, - }, - }; - const { modifiedCount } = await UserSession.updateMany({}, update); - - if (modifiedCount === 0) { - return []; - } - - return affectedUsers; -} diff --git a/apps/meteor/ee/server/services/presence/actions/setStatus.ts b/apps/meteor/ee/server/services/presence/actions/setStatus.ts deleted file mode 100755 index b8711ac1923..00000000000 --- a/apps/meteor/ee/server/services/presence/actions/setStatus.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { IUser, IUserSession, UserStatus } from '@rocket.chat/core-typings'; - -import { getCollection, Collections } from '../../mongo'; -import { processPresenceAndStatus } from '../lib/processConnectionStatus'; -import { api } from '../../../../../server/sdk/api'; - -export async function setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise<boolean> { - const query = { _id: uid }; - - const UserSession = await getCollection<IUserSession>(Collections.UserSession); - const userSessions = (await UserSession.findOne(query)) || { connections: [] }; - - const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); - - const update = { - statusDefault, - status, - statusConnection, - ...(typeof statusText !== 'undefined' - ? { - // TODO logic duplicated from Rocket.Chat core - statusText: String(statusText || '') - .trim() - .substr(0, 120), - } - : {}), - }; - - const User = await getCollection(Collections.User); - const result = await User.updateOne(query, { - $set: update, - }); - - if (result.modifiedCount > 0) { - const user = await User.findOne<IUser>(query, { projection: { username: 1 } }); - api.broadcast('presence.status', { - user: { _id: uid, username: user?.username, status, statusText }, - }); - } - - return !!result.modifiedCount; -} - -export async function setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> { - const query = { - '_id': uid, - 'connections.id': session, - }; - - const now = new Date(); - - const update = { - $set: { - 'connections.$.status': status, - 'connections.$._updatedAt': now, - }, - }; - - const UserSession = await getCollection(Collections.UserSession); - const result = await UserSession.updateOne(query, update); - - return !!result.modifiedCount; -} diff --git a/apps/meteor/ee/server/services/presence/actions/updateUserPresence.ts b/apps/meteor/ee/server/services/presence/actions/updateUserPresence.ts deleted file mode 100755 index d001acb3cf7..00000000000 --- a/apps/meteor/ee/server/services/presence/actions/updateUserPresence.ts +++ /dev/null @@ -1,42 +0,0 @@ -// import { afterAll } from '../hooks'; -import type { IUserSession, IUser } from '@rocket.chat/core-typings'; - -import { processPresenceAndStatus } from '../lib/processConnectionStatus'; -import { getCollection, Collections } from '../../mongo'; -import { api } from '../../../../../server/sdk/api'; - -const projection = { - projection: { - username: 1, - statusDefault: 1, - statusText: 1, - }, -}; - -export async function updateUserPresence(uid: string): Promise<void> { - const query = { _id: uid }; - - const UserSession = await getCollection<IUserSession>(Collections.UserSession); - const User = await getCollection<IUser>(Collections.User); - - const user = await User.findOne<IUser>(query, projection); - if (!user) { - return; - } - - const userSessions = (await UserSession.findOne(query)) || { connections: [] }; - - const { statusDefault } = user; - - const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); - - const result = await User.updateOne(query, { - $set: { status, statusConnection }, - }); - - if (result.modifiedCount > 0) { - api.broadcast('presence.status', { - user: { _id: uid, username: user.username, status, statusText: user.statusText }, - }); - } -} diff --git a/apps/meteor/ee/server/services/presence/service.ts b/apps/meteor/ee/server/services/presence/service.ts deleted file mode 100755 index 93908d7be80..00000000000 --- a/apps/meteor/ee/server/services/presence/service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '../../startup/broker'; - -import { api } from '../../../../server/sdk/api'; -import { Presence } from './Presence'; - -api.registerService(new Presence()); diff --git a/apps/meteor/imports/startup/server/index.js b/apps/meteor/imports/startup/server/index.ts similarity index 70% rename from apps/meteor/imports/startup/server/index.js rename to apps/meteor/imports/startup/server/index.ts index 91df55e9d4f..8b7c5bd8020 100644 --- a/apps/meteor/imports/startup/server/index.js +++ b/apps/meteor/imports/startup/server/index.ts @@ -1,3 +1,2 @@ import '../../message-read-receipt/server'; import '../../personal-access-tokens/server'; -import '../../users-presence/server'; diff --git a/apps/meteor/imports/users-presence/server/activeUsers.js b/apps/meteor/imports/users-presence/server/activeUsers.js deleted file mode 100644 index ab108fe3236..00000000000 --- a/apps/meteor/imports/users-presence/server/activeUsers.js +++ /dev/null @@ -1,36 +0,0 @@ -import { UserStatus } from '@rocket.chat/core-typings'; -import { UserPresenceEvents } from 'meteor/konecty:user-presence'; - -import { settings } from '../../../app/settings/server'; -import { api } from '../../../server/sdk/api'; - -export const STATUS_MAP = { - [UserStatus.OFFLINE]: 0, - [UserStatus.ONLINE]: 1, - [UserStatus.AWAY]: 2, - [UserStatus.BUSY]: 3, -}; - -export const setUserStatus = (user, status /* , statusConnection*/) => { - const { _id, username, statusText } = user; - - // since this callback can be called by only one instance in the cluster - // we need to broadcast the change to all instances - api.broadcast('presence.status', { - user: { status, _id, username, statusText }, // TODO remove username - }); -}; - -let TroubleshootDisablePresenceBroadcast; -settings.watch('Troubleshoot_Disable_Presence_Broadcast', (value) => { - if (TroubleshootDisablePresenceBroadcast === value) { - return; - } - TroubleshootDisablePresenceBroadcast = value; - - if (value) { - return UserPresenceEvents.removeListener('setUserStatus', setUserStatus); - } - - UserPresenceEvents.on('setUserStatus', setUserStatus); -}); diff --git a/apps/meteor/imports/users-presence/server/index.js b/apps/meteor/imports/users-presence/server/index.js deleted file mode 100644 index 8b7cdc0d71e..00000000000 --- a/apps/meteor/imports/users-presence/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './activeUsers'; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cd1588a5e36..ec59c061ba0 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -16,7 +16,7 @@ "start": "meteor", "build:ci": "METEOR_DISABLE_OPTIMISTIC_CACHING=1 meteor build --server-only", "dev": "meteor --exclude-archs \"web.browser.legacy, web.cordova\"", - "dsv": "meteor --exclude-archs \"web.browser.legacy, web.cordova\"", + "dsv": "meteor npm run dev", "ha": "meteor npm run ha:start", "ha:start": "ts-node .scripts/run-ha.ts main", "ha:add": "ts-node .scripts/run-ha.ts instance", @@ -183,7 +183,7 @@ "stylelint-order": "^5.0.0", "supertest": "^6.2.3", "template-file": "^6.0.1", - "ts-node": "^10.8.1", + "ts-node": "^10.9.1", "typescript": "~4.5.5", "webpack": "^4.46.0" }, @@ -223,6 +223,7 @@ "@rocket.chat/mp3-encoder": "0.24.0", "@rocket.chat/onboarding-ui": "next", "@rocket.chat/poplib": "workspace:^", + "@rocket.chat/presence": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", "@rocket.chat/ui-client": "workspace:^", diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index bfb5c200b16..f965b2f9854 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -6,7 +6,6 @@ import { LivechatInquiry, LivechatDepartmentAgents, Rooms, - UsersSessions, Roles, LoginServiceConfiguration, InstanceStatus, @@ -23,7 +22,6 @@ export const watchCollections = [ Subscriptions.getCollectionName(), LivechatInquiry.getCollectionName(), LivechatDepartmentAgents.getCollectionName(), - UsersSessions.getCollectionName(), Permissions.getCollectionName(), Roles.getCollectionName(), Rooms.getCollectionName(), diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 5f0898049c6..159b08e127a 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -66,6 +66,7 @@ import './methods/setUserActiveStatus'; import './methods/setUserPassword'; import './methods/toogleFavorite'; import './methods/unmuteUserInRoom'; +import './methods/userPresence'; import './methods/userSetUtcOffset'; import './publications/messages'; import './publications/room'; diff --git a/apps/meteor/server/methods/userPresence.ts b/apps/meteor/server/methods/userPresence.ts new file mode 100644 index 00000000000..1797dc839e2 --- /dev/null +++ b/apps/meteor/server/methods/userPresence.ts @@ -0,0 +1,28 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { Presence } from '../sdk'; + +Meteor.methods({ + 'UserPresence:setDefaultStatus'(status): Promise<boolean> | undefined { + const { userId } = this; + if (!userId) { + return; + } + return Presence.setStatus(userId, status); + }, + 'UserPresence:online'(): Promise<boolean> | undefined { + const { userId, connection } = this; + if (!userId || !connection) { + return; + } + return Presence.setConnectionStatus(userId, UserStatus.ONLINE, connection.id); + }, + 'UserPresence:away'(): Promise<boolean> | undefined { + const { userId, connection } = this; + if (!userId || !connection) { + return; + } + return Presence.setConnectionStatus(userId, UserStatus.AWAY, connection.id); + }, +}); diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 4c507ad3e8c..b223e390f98 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -736,6 +736,35 @@ export class UsersRaw extends BaseRaw { return this.update(query, update, { multi: true }); } + /** + * @param {string} userId + * @param {object} status + * @param {string} status.status + * @param {string} status.statusConnection + * @param {string} [status.statusDefault] + * @param {string} [status.statusText] + */ + updateStatusById(userId, { statusDefault, status, statusConnection, statusText }) { + const query = { + _id: userId, + }; + + const update = { + $set: { + status, + statusConnection, + ...(statusDefault && { statusDefault }), + ...(statusText && { + statusText: String(statusText).trim().substr(0, 120), + }), + }, + }; + + // We don't want to update the _updatedAt field on this operation, + // so we can check if the status update triggered a change + return this.col.updateOne(query, update); + } + openAgentsBusinessHoursByBusinessHourId(businessHourIds) { const query = { roles: 'livechat-agent', diff --git a/apps/meteor/server/models/raw/UsersSessions.ts b/apps/meteor/server/models/raw/UsersSessions.ts index 575cc16feff..02cfd889eb2 100644 --- a/apps/meteor/server/models/raw/UsersSessions.ts +++ b/apps/meteor/server/models/raw/UsersSessions.ts @@ -1,6 +1,6 @@ -import type { IUserSession, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IUserSession, IUserSessionConnection, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IUsersSessionsModel } from '@rocket.chat/model-typings'; -import type { Collection, Db } from 'mongodb'; +import type { FindCursor, Collection, Db, FindOptions } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -28,4 +28,104 @@ export class UsersSessionsRaw extends BaseRaw<IUserSession> implements IUsersSes }, ); } + + updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType<BaseRaw<IUserSession>['updateOne']> { + const query = { + '_id': uid, + 'connections.id': connectionId, + }; + + const update = { + $set: { + 'connections.$.status': status, + 'connections.$._updatedAt': new Date(), + }, + }; + + return this.updateOne(query, update); + } + + async removeConnectionsFromInstanceId(instanceId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> { + return this.updateMany( + { + 'connections.instanceId': instanceId, + }, + { + $pull: { + connections: { + instanceId, + }, + }, + }, + ); + } + + removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType<BaseRaw<IUserSession>['updateMany']> { + return this.updateMany( + {}, + { + $pull: { + connections: { + instanceId: { + $nin: instanceIds, + }, + }, + }, + }, + ); + } + + async removeConnectionByConnectionId(connectionId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> { + return this.updateMany( + { + 'connections.id': connectionId, + }, + { + $pull: { + connections: { + id: connectionId, + }, + }, + }, + ); + } + + findByInstanceId(instanceId: string): FindCursor<IUserSession> { + return this.find({ + 'connections.instanceId': instanceId, + }); + } + + addConnectionById( + userId: string, + { id, instanceId, status }: Pick<IUserSessionConnection, 'id' | 'instanceId' | 'status'>, + ): ReturnType<BaseRaw<IUserSession>['updateOne']> { + const now = new Date(); + + const update = { + $push: { + connections: { + id, + instanceId, + status, + _createdAt: now, + _updatedAt: now, + }, + }, + }; + + return this.updateOne({ _id: userId }, update, { upsert: true }); + } + + findByOtherInstanceIds(instanceIds: string[], options?: FindOptions<IUserSession>): FindCursor<IUserSession> { + return this.find( + { + 'connections.instanceId': { + $exists: true, + $nin: instanceIds, + }, + }, + options, + ); + } } diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index 1903fe818b0..c8390caddb7 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -16,7 +16,6 @@ import type { SettingValue, ILivechatInquiryRecord, IRole, - IUserSession, } from '@rocket.chat/core-typings'; import { Subscriptions, @@ -24,7 +23,6 @@ import { Users, Settings, Roles, - UsersSessions, LivechatInquiry, LivechatDepartmentAgents, Rooms, @@ -39,7 +37,6 @@ import { import { subscriptionFields, roomFields } from './publishFields'; import type { EventSignatures } from '../../sdk/lib/Events'; -import { isPresenceMonitorEnabled } from '../../lib/isPresenceMonitorEnabled'; import type { DatabaseWatcher } from '../../database/DatabaseWatcher'; type BroadcastCallback = <T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>) => Promise<void>; @@ -160,25 +157,6 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb }); }); - if (isPresenceMonitorEnabled()) { - watcher.on<IUserSession>(UsersSessions.getCollectionName(), async ({ clientAction, id, data: eventData }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - const data = eventData ?? (await UsersSessions.findOneById(id)); - if (!data) { - return; - } - - broadcast('watch.userSessions', { clientAction, userSession: data }); - break; - case 'removed': - broadcast('watch.userSessions', { clientAction, userSession: { _id: id } }); - break; - } - }); - } - watcher.on<ILivechatInquiryRecord>(LivechatInquiry.getCollectionName(), async ({ clientAction, id, data, diff }) => { switch (clientAction) { case 'inserted': diff --git a/apps/meteor/server/sdk/api.ts b/apps/meteor/server/sdk/api.ts index 7255bd074d8..4d7df7db62c 100644 --- a/apps/meteor/server/sdk/api.ts +++ b/apps/meteor/server/sdk/api.ts @@ -1,3 +1,9 @@ +import { isRunningMs } from '../lib/isRunningMs'; import { Api } from './lib/Api'; +import { LocalBroker } from './lib/LocalBroker'; export const api = new Api(); + +if (!isRunningMs()) { + api.setBroker(new LocalBroker()); +} diff --git a/apps/meteor/server/sdk/lib/Api.ts b/apps/meteor/server/sdk/lib/Api.ts index c650e6e3daa..be2f6729648 100644 --- a/apps/meteor/server/sdk/lib/Api.ts +++ b/apps/meteor/server/sdk/lib/Api.ts @@ -1,13 +1,12 @@ -// import { BaseBroker } from './BaseBroker'; -import type { IBroker } from '../types/IBroker'; -import type { ServiceClass } from '../types/ServiceClass'; +import type { IApiService } from '../types/IApiService'; +import type { IBroker, IBrokerNode } from '../types/IBroker'; +import type { IServiceClass } from '../types/ServiceClass'; import type { EventSignatures } from './Events'; -import { LocalBroker } from './LocalBroker'; -export class Api { - private services = new Set<ServiceClass>(); +export class Api implements IApiService { + private services: Set<IServiceClass> = new Set<IServiceClass>(); - private broker: IBroker = new LocalBroker(); + private broker: IBroker; // set a broker for the API and registers all services in the broker setBroker(broker: IBroker): void { @@ -16,7 +15,7 @@ export class Api { this.services.forEach((service) => this.broker.createService(service)); } - destroyService(instance: ServiceClass): void { + destroyService(instance: IServiceClass): void { if (!this.services.has(instance)) { return; } @@ -28,9 +27,11 @@ export class Api { this.services.delete(instance); } - registerService(instance: ServiceClass): void { + registerService(instance: IServiceClass): void { this.services.add(instance); + instance.setApi(this); + if (this.broker) { this.broker.createService(instance); } @@ -59,4 +60,8 @@ export class Api { async broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> { return this.broker.broadcastLocal(event, ...args); } + + nodeList(): Promise<IBrokerNode[]> { + return this.broker.nodeList(); + } } diff --git a/apps/meteor/server/sdk/lib/Events.ts b/apps/meteor/server/sdk/lib/Events.ts index 4c683f62aa5..6b4336d6d70 100644 --- a/apps/meteor/server/sdk/lib/Events.ts +++ b/apps/meteor/server/sdk/lib/Events.ts @@ -17,12 +17,12 @@ import type { ISocketConnection, ISubscription, IUser, - IUserSession, IUserStatus, IInvite, IWebdavAccount, ICustomSound, VoipEventDataSignature, + UserStatus, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from '../types/IMeteor'; @@ -88,12 +88,14 @@ export type EventSignatures = { 'user.nameChanged'(user: Partial<IUser>): void; 'user.roleUpdate'(update: Record<string, any>): void; 'user.updateCustomStatus'(userStatus: IUserStatus): void; - 'presence.status'(data: { user: Partial<IUser> }): void; + 'presence.status'(data: { + user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'name' | 'roles'>; + previousStatus: UserStatus | undefined; + }): void; 'watch.messages'(data: { clientAction: ClientAction; message: Partial<IMessage> }): void; 'watch.roles'(data: { clientAction: ClientAction; role: Partial<IRole> }): void; 'watch.rooms'(data: { clientAction: ClientAction; room: Pick<IRoom, '_id'> & Partial<IRoom> }): void; 'watch.subscriptions'(data: { clientAction: ClientAction; subscription: Partial<ISubscription> }): void; - 'watch.userSessions'(data: { clientAction: ClientAction; userSession: Partial<IUserSession> }): void; 'watch.inquiries'(data: { clientAction: ClientAction; inquiry: IInquiry; diff?: undefined | Record<string, any> }): void; 'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void; 'watch.users'(data: { diff --git a/apps/meteor/server/sdk/lib/LocalBroker.ts b/apps/meteor/server/sdk/lib/LocalBroker.ts index 13ab33de044..a63490d4cf8 100644 --- a/apps/meteor/server/sdk/lib/LocalBroker.ts +++ b/apps/meteor/server/sdk/lib/LocalBroker.ts @@ -1,7 +1,9 @@ import { EventEmitter } from 'events'; +import { InstanceStatus } from '@rocket.chat/models'; + import type { IBroker, IBrokerNode } from '../types/IBroker'; -import type { ServiceClass } from '../types/ServiceClass'; +import type { ServiceClass, IServiceClass } from '../types/ServiceClass'; import { asyncLocalStorage } from '..'; import type { EventSignatures } from './Events'; import { StreamerCentral } from '../../modules/streamer/streamer.module'; @@ -49,9 +51,11 @@ export class LocalBroker implements IBroker { } } - createService(instance: ServiceClass): void { + createService(instance: IServiceClass): void { const namespace = instance.getName(); + instance.created(); + instance.getEvents().forEach((eventName) => { this.events.on(eventName, (...args) => { instance.emit(eventName, ...(args as Parameters<EventSignatures[typeof eventName]>)); @@ -70,6 +74,8 @@ export class LocalBroker implements IBroker { this.methods.set(`${namespace}.${method}`, i[method].bind(i)); } + + instance.started(); } async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> { @@ -91,6 +97,9 @@ export class LocalBroker implements IBroker { } async nodeList(): Promise<IBrokerNode[]> { - return []; + // TODO models should not be called form here. we should create an abstraction to an internal service to perform this query + const instances = await InstanceStatus.find({}, { projection: { _id: 1 } }).toArray(); + + return instances.map(({ _id }) => ({ id: _id, available: true })); } } diff --git a/apps/meteor/server/sdk/types/IApiService.ts b/apps/meteor/server/sdk/types/IApiService.ts new file mode 100644 index 00000000000..fad6c768fce --- /dev/null +++ b/apps/meteor/server/sdk/types/IApiService.ts @@ -0,0 +1,27 @@ +import type { IBroker, IBrokerNode } from './IBroker'; +import type { IServiceClass } from './ServiceClass'; +import type { EventSignatures } from '../lib/Events'; + +export interface IApiService { + setBroker(broker: IBroker): void; + + destroyService(instance: IServiceClass): void; + + registerService(instance: IServiceClass): void; + + call(method: string, data?: unknown): Promise<any>; + + waitAndCall(method: string, data: any): Promise<any>; + + broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>; + + broadcastToServices<T extends keyof EventSignatures>( + services: string[], + event: T, + ...args: Parameters<EventSignatures[T]> + ): Promise<void>; + + broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>; + + nodeList(): Promise<IBrokerNode[]>; +} diff --git a/apps/meteor/server/sdk/types/IAppsEngineService.ts b/apps/meteor/server/sdk/types/IAppsEngineService.ts new file mode 100644 index 00000000000..c0923978ec6 --- /dev/null +++ b/apps/meteor/server/sdk/types/IAppsEngineService.ts @@ -0,0 +1,3 @@ +import type { IServiceClass } from './ServiceClass'; + +export type IAppsEngineService = IServiceClass; diff --git a/apps/meteor/server/sdk/types/IBroker.ts b/apps/meteor/server/sdk/types/IBroker.ts index dcb561e5148..910432cd52e 100644 --- a/apps/meteor/server/sdk/types/IBroker.ts +++ b/apps/meteor/server/sdk/types/IBroker.ts @@ -1,18 +1,18 @@ -import type { ServiceClass } from './ServiceClass'; +import type { IServiceClass } from './ServiceClass'; import type { EventSignatures } from '../lib/Events'; export interface IBrokerNode { id: string; - instanceID: string; + instanceID?: string; available: boolean; - local: boolean; + local?: boolean; // lastHeartbeatTime: 16, // config: {}, // client: { type: 'nodejs', version: '0.14.10', langVersion: 'v12.18.3' }, // metadata: {}, // ipList: [ '192.168.0.100', '192.168.1.25' ], // port: 59989, - // hostname: 'RocketChats-MacBook-Pro-Rodrigo-Nascimento.local', + // hostname: 'service.local-1', // udpAddress: null, // cpu: 25, // cpuSeq: 1, @@ -47,8 +47,8 @@ export interface IServiceMetrics { export interface IBroker { metrics?: IServiceMetrics; - destroyService(service: ServiceClass): void; - createService(service: ServiceClass): void; + destroyService(service: IServiceClass): void; + createService(service: IServiceClass): void; call(method: string, data: any): Promise<any>; waitAndCall(method: string, data: any): Promise<any>; broadcastToServices<T extends keyof EventSignatures>( diff --git a/apps/meteor/server/sdk/types/IOmnichannelService.ts b/apps/meteor/server/sdk/types/IOmnichannelService.ts new file mode 100644 index 00000000000..d5aaae341da --- /dev/null +++ b/apps/meteor/server/sdk/types/IOmnichannelService.ts @@ -0,0 +1,3 @@ +import type { IServiceClass } from './ServiceClass'; + +export type IOmnichannelService = IServiceClass; diff --git a/apps/meteor/server/sdk/types/IPresence.ts b/apps/meteor/server/sdk/types/IPresence.ts index c495580a214..159876907f6 100644 --- a/apps/meteor/server/sdk/types/IPresence.ts +++ b/apps/meteor/server/sdk/types/IPresence.ts @@ -3,10 +3,19 @@ import type { UserStatus } from '@rocket.chat/core-typings'; import type { IServiceClass } from './ServiceClass'; export interface IPresence extends IServiceClass { - newConnection(uid: string, session: string): Promise<{ uid: string; connectionId: string } | undefined>; - removeConnection(uid: string, session: string): Promise<{ uid: string; session: string }>; + newConnection( + uid: string | undefined, + session: string | undefined, + nodeId: string, + ): Promise<{ uid: string; connectionId: string } | undefined>; + removeConnection( + uid: string | undefined, + session: string | undefined, + nodeId: string, + ): Promise<{ uid: string; session: string } | undefined>; removeLostConnections(nodeID: string): Promise<string[]>; setStatus(uid: string, status: UserStatus, statusText?: string): Promise<boolean>; setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean>; updateUserPresence(uid: string): Promise<void>; + toggleBroadcast(enabled: boolean): void; } diff --git a/apps/meteor/server/sdk/types/ServiceClass.ts b/apps/meteor/server/sdk/types/ServiceClass.ts index 23d0f835769..8e9a6d65c54 100644 --- a/apps/meteor/server/sdk/types/ServiceClass.ts +++ b/apps/meteor/server/sdk/types/ServiceClass.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; import { asyncLocalStorage } from '..'; import type { IBroker, IBrokerNode } from './IBroker'; import type { EventSignatures } from '../lib/Events'; +import type { IApiService } from './IApiService'; export interface IServiceContext { id: string; // Context ID @@ -29,12 +30,18 @@ export interface IServiceClass { onNodeConnected?({ node, reconnected }: { node: IBrokerNode; reconnected: boolean }): void; onNodeUpdated?({ node }: { node: IBrokerNode }): void; onNodeDisconnected?({ node, unexpected }: { node: IBrokerNode; unexpected: boolean }): Promise<void>; + getEvents(): Array<keyof EventSignatures>; + + setApi(api: IApiService): void; onEvent<T extends keyof EventSignatures>(event: T, handler: EventSignatures[T]): void; + emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void; + + isInternal(): boolean; - created?(): Promise<void>; - started?(): Promise<void>; - stopped?(): Promise<void>; + created(): Promise<void>; + started(): Promise<void>; + stopped(): Promise<void>; } export abstract class ServiceClass implements IServiceClass { @@ -44,10 +51,16 @@ export abstract class ServiceClass implements IServiceClass { protected internal = false; + protected api: IApiService; + constructor() { this.emit = this.emit.bind(this); } + setApi(api: IApiService): void { + this.api = api; + } + getEvents(): Array<keyof EventSignatures> { return this.events.eventNames() as unknown as Array<keyof EventSignatures>; } @@ -71,6 +84,18 @@ export abstract class ServiceClass implements IServiceClass { public emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void { this.events.emit(event, ...args); } + + async created(): Promise<void> { + // noop + } + + async started(): Promise<void> { + // noop + } + + async stopped(): Promise<void> { + // noop + } } /** diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts new file mode 100644 index 00000000000..f8485299e9a --- /dev/null +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -0,0 +1,17 @@ +import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; +import type { IAppsEngineService } from '../../sdk/types/IAppsEngineService'; +import { Apps, AppEvents } from '../../../app/apps/server/orchestrator'; + +export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { + protected name = 'apps-engine'; + + async created() { + this.onEvent('presence.status', async ({ user, previousStatus }): Promise<void> => { + Apps.triggerEvent(AppEvents.IPostUserStatusChanged, { + user, + currentStatus: user.status, + previousStatus, + }); + }); + } +} diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index bea9d21f891..8a8b14ac8a7 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { UserPresenceMonitor, UserPresence } from 'meteor/konecty:user-presence'; import { MongoInternals } from 'meteor/mongo'; import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -16,10 +15,9 @@ import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager' import { onlineAgents, monitorAgents } from '../../../app/livechat/server/lib/stream/agentStatus'; import { matrixBroadCastActions } from '../../stream/streamBroadcast'; import { triggerHandler } from '../../../app/integrations/server/lib/triggerHandler'; -import { ListenersModule, minimongoChangeMap } from '../../modules/listeners/listeners.module'; +import { ListenersModule } from '../../modules/listeners/listeners.module'; import notifications from '../../../app/notifications/server/lib/Notifications'; import { configureEmailInboxes } from '../../features/EmailInbox/EmailInbox'; -import { isPresenceMonitorEnabled } from '../../lib/isPresenceMonitorEnabled'; import { use } from '../../../app/settings/server/Middleware'; import type { IRoutingManagerConfig } from '../../../definition/IRoutingManagerConfig'; @@ -148,38 +146,14 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { setValue(setting._id, undefined); }); - // TODO: May need to merge with https://github.com/RocketChat/Rocket.Chat/blob/0ddc2831baf8340cbbbc432f88fc2cb97be70e9b/ee/server/services/Presence/Presence.ts#L28 - if (isPresenceMonitorEnabled()) { - this.onEvent('watch.userSessions', async ({ clientAction, userSession }): Promise<void> => { - if (clientAction === 'removed') { - UserPresenceMonitor.processUserSession( - { - _id: userSession._id, - connections: [ - { - fake: true, - }, - ], - }, - 'removed', - ); - } - - UserPresenceMonitor.processUserSession(userSession, minimongoChangeMap[clientAction]); - }); - } - this.onEvent('watch.instanceStatus', async ({ clientAction, id, data }): Promise<void> => { if (clientAction === 'removed') { - UserPresence.removeConnectionsByInstanceId(id); matrixBroadCastActions?.removed?.(id); return; } - if (clientAction === 'inserted') { - if (data?.extraInformation?.port) { - matrixBroadCastActions?.added?.(data); - } + if (clientAction === 'inserted' && data?.extraInformation?.port) { + matrixBroadCastActions?.added?.(data); } }); diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts new file mode 100644 index 00000000000..bf0063446f8 --- /dev/null +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -0,0 +1,20 @@ +import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; +import type { IOmnichannelService } from '../../sdk/types/IOmnichannelService'; +import { Livechat } from '../../../app/livechat/server'; + +export class OmnichannelService extends ServiceClassInternal implements IOmnichannelService { + protected name = 'omnichannel'; + + async created() { + this.onEvent('presence.status', async ({ user }): Promise<void> => { + if (!user?._id) { + return; + } + const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); + if (hasRole) { + // TODO change `Livechat.notifyAgentStatusChanged` to a service call + Livechat.notifyAgentStatusChanged(user._id, user.status); + } + }); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index da0c9e6154f..6f389cf593d 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -2,6 +2,7 @@ import { MongoInternals } from 'meteor/mongo'; import { AnalyticsService } from './analytics/service'; import { api } from '../sdk/api'; +import { AppsEngineService } from './apps-engine/service'; import { AuthorizationLivechat } from '../../app/livechat/server/roomAccessValidator.internalService'; import { BannerService } from './banner/service'; import { LDAPService } from './ldap/service'; @@ -12,6 +13,7 @@ import { RoomService } from './room/service'; import { SAUMonitorService } from './sauMonitor/service'; import { TeamService } from './team/service'; import { UiKitCoreApp } from './uikit-core-app/service'; +import { OmnichannelService } from './omnichannel/service'; import { OmnichannelVoipService } from './omnichannel-voip/service'; import { VoipService } from './voip/service'; import { VideoConfService } from './video-conference/service'; @@ -21,6 +23,7 @@ import { DeviceManagementService } from './device-management/service'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; +api.registerService(new AppsEngineService()); api.registerService(new AnalyticsService()); api.registerService(new AuthorizationLivechat()); api.registerService(new BannerService()); @@ -31,6 +34,7 @@ api.registerService(new NPSService()); api.registerService(new RoomService()); api.registerService(new SAUMonitorService()); api.registerService(new VoipService(db)); +api.registerService(new OmnichannelService()); api.registerService(new OmnichannelVoipService()); api.registerService(new TeamService()); api.registerService(new UiKitCoreApp()); @@ -41,8 +45,11 @@ api.registerService(new VideoConfService()); // if the process is running in micro services mode we don't need to register services that will run separately if (!isRunningMs()) { (async (): Promise<void> => { + const { Presence } = await import('@rocket.chat/presence'); + const { Authorization } = await import('./authorization/service'); + api.registerService(new Presence()); api.registerService(new Authorization(db)); })(); } diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 2460b840702..44f291a9f82 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -1,12 +1,18 @@ import './migrations'; -import './watchDb'; import './appcache'; import './callbacks'; import './cron'; import './initialData'; import './instance'; -import './presence'; import './serverRunning'; import './coreApps'; +import './presenceTroubleshoot'; import '../hooks'; import '../lib/rooms/roomTypes'; +import { isRunningMs } from '../lib/isRunningMs'; + +// only starts network broker if running in micro services mode +if (!isRunningMs()) { + require('./watchDb'); + require('./presence'); +} diff --git a/apps/meteor/server/startup/presence.js b/apps/meteor/server/startup/presence.js deleted file mode 100644 index dbc79e7603a..00000000000 --- a/apps/meteor/server/startup/presence.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { UserPresence } from 'meteor/konecty:user-presence'; -import { InstanceStatus, UsersSessions } from '@rocket.chat/models'; - -import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; - -Meteor.startup(function () { - UserPresence.start(); - - if (!isPresenceMonitorEnabled()) { - return; - } - // UserPresenceMonitor.start(); - - // Remove lost connections - const ids = Promise.await(InstanceStatus.find({}, { projection: { _id: 1 } }).toArray()).map((id) => id._id); - - Promise.await(UsersSessions.clearConnectionsFromInstanceId(ids)); -}); diff --git a/apps/meteor/server/startup/presence.ts b/apps/meteor/server/startup/presence.ts new file mode 100644 index 00000000000..e56bb7283bb --- /dev/null +++ b/apps/meteor/server/startup/presence.ts @@ -0,0 +1,35 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; +import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; + +import { Presence } from '../sdk'; + +Meteor.startup(function () { + const nodeId = InstanceStatus.id(); + Meteor.onConnection(function (connection) { + const session = Meteor.server.sessions.get(connection.id); + + connection.onClose(function () { + if (!session) { + return; + } + + Presence.removeConnection(session.userId, connection.id, nodeId); + }); + }); + + process.on('exit', function () { + Presence.removeLostConnections(nodeId); + }); + + Accounts.onLogin(function (login: any): void { + if (login.type !== 'resume') { + return; + } + Presence.newConnection(login.user._id, login.connection.id, nodeId); + }); + + Accounts.onLogout(function (login: any): void { + Presence.removeConnection(login.user._id, login.connection.id, nodeId); + }); +}); diff --git a/apps/meteor/server/startup/presenceTroubleshoot.ts b/apps/meteor/server/startup/presenceTroubleshoot.ts new file mode 100644 index 00000000000..6b949dd0239 --- /dev/null +++ b/apps/meteor/server/startup/presenceTroubleshoot.ts @@ -0,0 +1,11 @@ +import { settings } from '../../app/settings/server'; +import { Presence } from '../sdk'; + +// maybe this setting should disable the listener to 'presence.status' event on listerners.module.ts +settings.watch('Troubleshoot_Disable_Presence_Broadcast', async function (value) { + try { + await Presence.toggleBroadcast(!value); + } catch (e) { + // do nothing + } +}); diff --git a/apps/meteor/server/startup/watchDb.ts b/apps/meteor/server/startup/watchDb.ts index 9690b3d1c8d..5c26fdcd2cf 100644 --- a/apps/meteor/server/startup/watchDb.ts +++ b/apps/meteor/server/startup/watchDb.ts @@ -2,17 +2,14 @@ import { MongoInternals } from 'meteor/mongo'; import { DatabaseWatcher } from '../database/DatabaseWatcher'; import { db } from '../database/utils'; -import { isRunningMs } from '../lib/isRunningMs'; import { initWatchers } from '../modules/watchers/watchers.module'; import { api } from '../sdk/api'; import { metrics } from '../../app/metrics/server/lib/metrics'; -if (!isRunningMs()) { - const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); +const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); - const watcher = new DatabaseWatcher({ db, _oplogHandle: (mongo as any)._oplogHandle, metrics }); +const watcher = new DatabaseWatcher({ db, _oplogHandle: (mongo as any)._oplogHandle, metrics }); - initWatchers(watcher, api.broadcastLocal.bind(api)); +initWatchers(watcher, api.broadcastLocal.bind(api)); - watcher.watch(); -} +watcher.watch(); diff --git a/apps/meteor/server/stream/streamBroadcast.js b/apps/meteor/server/stream/streamBroadcast.js index 47a9412068c..0eb744e04dd 100644 --- a/apps/meteor/server/stream/streamBroadcast.js +++ b/apps/meteor/server/stream/streamBroadcast.js @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { UserPresence } from 'meteor/konecty:user-presence'; import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; import { check } from 'meteor/check'; import { DDP } from 'meteor/ddp'; @@ -10,9 +9,7 @@ import { Logger } from '../lib/logger/Logger'; import { hasPermission } from '../../app/authorization/server'; import { settings } from '../../app/settings/server'; import { isDocker, getURL } from '../../app/utils/server'; -import { Users } from '../../app/models/server'; import { StreamerCentral } from '../modules/streamer/streamer.module'; -import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; process.env.PORT = String(process.env.PORT).trim(); process.env.INSTANCE_IP = String(process.env.INSTANCE_IP).trim(); @@ -57,13 +54,8 @@ function authorizeConnection(instance) { } const cache = new Map(); -const originalSetDefaultStatus = UserPresence.setDefaultStatus; export let matrixBroadCastActions; function startMatrixBroadcast() { - if (!isPresenceMonitorEnabled()) { - UserPresence.setDefaultStatus = originalSetDefaultStatus; - } - matrixBroadCastActions = { added: Meteor.bindEnvironment((record) => { cache.set(record._id, record); @@ -156,12 +148,6 @@ function startStreamCastBroadcast(value) { connLogger.info({ msg: 'connecting in', instance, value }); - if (!isPresenceMonitorEnabled()) { - UserPresence.setDefaultStatus = (id, status) => { - Users.updateDefaultStatus(id, status); - }; - } - const connection = DDP.connect(value, { _dontPrintErrors: settings.get('Log_Level') !== '2', }); diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 32b33c3d0a4..3444c4300fa 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -56,9 +56,9 @@ services: presence-service: build: - dockerfile: apps/meteor/ee/server/services/Dockerfile + dockerfile: ee/apps/presence-service/Dockerfile args: - SERVICE: presence + SERVICE: presence-service image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG} environment: - MONGO_URL=${MONGO_URL} diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index f4f7d764cad..98373c82dec 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -5,6 +5,7 @@ "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint src", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -47,6 +48,7 @@ "@types/ws": "^8.5.3", "eslint": "^8.21.0", "pino-pretty": "^7.6.1", + "ts-node": "^10.9.1", "typescript": "~4.5.5" }, "main": "./dist/service.js", diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 4cd35dd398e..f0c873e90b7 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -6,8 +6,9 @@ import WebSocket from 'ws'; import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module'; import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module'; -import { MeteorService } from '../../../../apps/meteor/server/sdk'; +import { MeteorService, Presence } from '../../../../apps/meteor/server/sdk'; import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; +import { api } from '../../../../apps/meteor/server/sdk/api'; import { Client } from './Client'; import { events, server } from './configureServer'; import { DDP_EVENTS } from './constants'; @@ -104,7 +105,7 @@ export class DDPStreamer extends ServiceClass { } const { broker, nodeID } = this.context; - if (!broker) { + if (!broker || !nodeID) { return; } @@ -141,5 +142,38 @@ export class DDPStreamer extends ServiceClass { metrics.decrement('users_logged', { nodeID }, 1); } }); + + server.on(DDP_EVENTS.LOGGED, (info) => { + const { userId, connection } = info; + + Presence.newConnection(userId, connection.id, nodeID); + api.broadcast('accounts.login', { userId, connection }); + }); + + server.on(DDP_EVENTS.LOGGEDOUT, (info) => { + const { userId, connection } = info; + + api.broadcast('accounts.logout', { userId, connection }); + + if (!userId) { + return; + } + Presence.removeConnection(userId, connection.id, nodeID); + }); + + server.on(DDP_EVENTS.DISCONNECTED, (info) => { + const { userId, connection } = info; + + api.broadcast('socket.disconnected', connection); + + if (!userId) { + return; + } + Presence.removeConnection(userId, connection.id, nodeID); + }); + + server.on(DDP_EVENTS.CONNECTED, ({ connection }) => { + api.broadcast('socket.connected', connection); + }); } } diff --git a/ee/apps/ddp-streamer/src/configureServer.ts b/ee/apps/ddp-streamer/src/configureServer.ts index 6011f7291d0..d8165ca1156 100644 --- a/ee/apps/ddp-streamer/src/configureServer.ts +++ b/ee/apps/ddp-streamer/src/configureServer.ts @@ -5,7 +5,6 @@ import { UserStatus } from '@rocket.chat/core-typings'; import { DDP_EVENTS, WS_ERRORS } from './constants'; import { Account, Presence, MeteorService } from '../../../../apps/meteor/server/sdk'; import { Server } from './Server'; -import { api } from '../../../../apps/meteor/server/sdk/api'; import { MeteorError } from '../../../../apps/meteor/server/sdk/errors'; import { Autoupdate } from './lib/Autoupdate'; @@ -74,6 +73,7 @@ server.methods({ this.userId = result.uid; this.userToken = result.hashedToken; + this.connection.loginToken = result.hashedToken; this.emit(DDP_EVENTS.LOGGED); @@ -152,36 +152,3 @@ server.methods({ } }, }); - -server.on(DDP_EVENTS.LOGGED, (info) => { - const { userId, connection } = info; - - Presence.newConnection(userId, connection.id); - api.broadcast('accounts.login', { userId, connection }); -}); - -server.on(DDP_EVENTS.LOGGEDOUT, (info) => { - const { userId, connection } = info; - - api.broadcast('accounts.logout', { userId, connection }); - - if (!userId) { - return; - } - Presence.removeConnection(userId, connection.id); -}); - -server.on(DDP_EVENTS.DISCONNECTED, (info) => { - const { userId, connection } = info; - - api.broadcast('socket.disconnected', connection); - - if (!userId) { - return; - } - Presence.removeConnection(userId, connection.id); -}); - -server.on(DDP_EVENTS.CONNECTED, ({ connection }) => { - api.broadcast('socket.connected', connection); -}); diff --git a/ee/apps/presence-service/.eslintrc b/ee/apps/presence-service/.eslintrc new file mode 100644 index 00000000000..4d3f4a7d4d5 --- /dev/null +++ b/ee/apps/presence-service/.eslintrc @@ -0,0 +1,16 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"], + "plugins": ["jest"], + "env": { + "jest/globals": true + } +} diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile new file mode 100644 index 00000000000..b1e116ee833 --- /dev/null +++ b/ee/apps/presence-service/Dockerfile @@ -0,0 +1,38 @@ +FROM node:14.19.3-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/presence/package.json packages/presence/package.json +COPY ./packages/presence/dist packages/presence/dist +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist +COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json +COPY ./packages/ui-contexts/dist packages/ui-contexts/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json new file mode 100644 index 00000000000..f99c3b09fa2 --- /dev/null +++ b/ee/apps/presence-service/package.json @@ -0,0 +1,43 @@ +{ + "name": "@rocket.chat/presence-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat Presence service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/emitter": "next", + "@rocket.chat/presence": "workspace:^", + "@rocket.chat/string-helpers": "next", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.1", + "moleculer": "^0.14.21", + "mongodb": "^4.3.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/eslint": "^8", + "@types/polka": "^0.5.4", + "eslint": "^8.21.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/presence/src/Presence.js", + "files": [ + "/dist" + ] +} diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts new file mode 100755 index 00000000000..e0cec05d441 --- /dev/null +++ b/ee/apps/presence-service/src/service.ts @@ -0,0 +1,28 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; + +import '../../../../apps/meteor/ee/server/startup/broker'; + +import { api } from '../../../../apps/meteor/server/sdk/api'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3031; + +getConnection().then(async (db) => { + const trash = await getCollection<Document>(Collections.Trash); + + registerServiceModels(db, trash); + + // need to import Presence service after models are registered + const { Presence } = await import('@rocket.chat/presence'); + + api.registerService(new Presence()); + + polka() + .get('/health', async function (_req, res) { + await api.nodeList(); + res.end('ok'); + }) + .listen(PORT); +}); diff --git a/ee/apps/presence-service/tsconfig.json b/ee/apps/presence-service/tsconfig.json new file mode 100644 index 00000000000..8893cea8ceb --- /dev/null +++ b/ee/apps/presence-service/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition"], + "exclude": ["./dist"] +} diff --git a/packages/core-typings/src/ISocketConnection.ts b/packages/core-typings/src/ISocketConnection.ts index c3f652c8640..8f1c5664db5 100644 --- a/packages/core-typings/src/ISocketConnection.ts +++ b/packages/core-typings/src/ISocketConnection.ts @@ -3,6 +3,7 @@ import type { IncomingHttpHeaders } from 'http'; export interface ISocketConnection { id: string; instanceId: string; + loginToken?: string; livechatToken?: string; onClose(fn: (...args: any[]) => void): void; clientAddress: string | undefined; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 0119392fb93..62364f97c32 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,5 +1,5 @@ import type { Document, UpdateResult, FindCursor, FindOptions } from 'mongodb'; -import type { IUser, IRole, IRoom, ILivechatAgent } from '@rocket.chat/core-typings'; +import type { IUser, IRole, IRoom, ILivechatAgent, UserStatus } from '@rocket.chat/core-typings'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -153,4 +153,14 @@ export interface IUsersModel extends IBaseModel<IUser> { removeRoomByRoomId(rid: any): any; findOneByResetToken(token: string, options: FindOptions<IUser>): Promise<IUser | null>; + + updateStatusById( + userId: string, + { + statusDefault, + status, + statusConnection, + statusText, + }: { statusDefault?: string; status: UserStatus; statusConnection: UserStatus; statusText?: string }, + ): Promise<UpdateResult>; } diff --git a/packages/model-typings/src/models/IUsersSessionsModel.ts b/packages/model-typings/src/models/IUsersSessionsModel.ts index 9c940129ee0..4e4ef843f9b 100644 --- a/packages/model-typings/src/models/IUsersSessionsModel.ts +++ b/packages/model-typings/src/models/IUsersSessionsModel.ts @@ -1,7 +1,18 @@ -import type { IUserSession } from '@rocket.chat/core-typings'; +import type { FindCursor, FindOptions } from 'mongodb'; +import type { IUserSession, IUserSessionConnection } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; export interface IUsersSessionsModel extends IBaseModel<IUserSession> { clearConnectionsFromInstanceId(instanceId: string[]): ReturnType<IBaseModel<IUserSession>['updateMany']>; + updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType<IBaseModel<IUserSession>['updateOne']>; + removeConnectionsFromInstanceId(instanceId: string): ReturnType<IBaseModel<IUserSession>['updateMany']>; + removeConnectionByConnectionId(connectionId: string): ReturnType<IBaseModel<IUserSession>['updateMany']>; + findByInstanceId(instanceId: string): FindCursor<IUserSession>; + addConnectionById( + userId: string, + { id, instanceId, status }: Pick<IUserSessionConnection, 'id' | 'instanceId' | 'status'>, + ): ReturnType<IBaseModel<IUserSession>['updateOne']>; + findByOtherInstanceIds(instanceIds: string[], options?: FindOptions<IUserSession>): FindCursor<IUserSession>; + removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType<IBaseModel<IUserSession>['updateMany']>; } diff --git a/packages/models/src/proxify.ts b/packages/models/src/proxify.ts index e81c97e6743..9f9422445b9 100644 --- a/packages/models/src/proxify.ts +++ b/packages/models/src/proxify.ts @@ -7,7 +7,10 @@ function handler<T extends object>(namespace: string): ProxyHandler<T> { return { get: (_target: T, prop: string): any => { if (!models.has(namespace) && lazyModels.has(namespace)) { - models.set(namespace, (lazyModels.get(namespace) as () => IBaseModel<any>)()); + const getModel = lazyModels.get(namespace); + if (getModel) { + models.set(namespace, getModel()); + } } // @ts-ignore diff --git a/packages/presence/.eslintrc b/packages/presence/.eslintrc new file mode 100644 index 00000000000..56a6f6602e3 --- /dev/null +++ b/packages/presence/.eslintrc @@ -0,0 +1,12 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/presence/.gitignore b/packages/presence/.gitignore new file mode 100644 index 00000000000..996e8eedb9a --- /dev/null +++ b/packages/presence/.gitignore @@ -0,0 +1 @@ +.nyc_output diff --git a/packages/presence/babel.config.js b/packages/presence/babel.config.js new file mode 100644 index 00000000000..7672dadf24c --- /dev/null +++ b/packages/presence/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], +}; diff --git a/packages/presence/package.json b/packages/presence/package.json new file mode 100644 index 00000000000..7f98deef502 --- /dev/null +++ b/packages/presence/package.json @@ -0,0 +1,37 @@ +{ + "name": "@rocket.chat/presence", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@babel/core": "^7.19.1", + "@babel/preset-env": "^7.19.1", + "@babel/preset-typescript": "^7.18.6", + "@rocket.chat/apps-engine": "^1.32.0", + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", + "@types/node": "^14.18.21", + "babel-jest": "^29.0.3", + "eslint": "^8.21.0", + "jest": "^29.0.3", + "typescript": "~4.5.5" + }, + "scripts": { + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "jest": "jest", + "build": "tsc -p tsconfig.json", + "testunit": "jest tests/**/*.test.ts", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "main": "./dist/packages/presence/src/Presence.js", + "typings": "./dist/packages/presence/src/Presence.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "mongodb": "^4.3.1" + } +} diff --git a/packages/presence/src/Presence.ts b/packages/presence/src/Presence.ts new file mode 100755 index 00000000000..7e6a3bf70cf --- /dev/null +++ b/packages/presence/src/Presence.ts @@ -0,0 +1,189 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { Users, UsersSessions } from '@rocket.chat/models'; + +import { processPresenceAndStatus } from './lib/processConnectionStatus'; +import type { IPresence } from '../../../apps/meteor/server/sdk/types/IPresence'; +import type { IBrokerNode } from '../../../apps/meteor/server/sdk/types/IBroker'; +import { ServiceClass } from '../../../apps/meteor/server/sdk/types/ServiceClass'; + +export class Presence extends ServiceClass implements IPresence { + protected name = 'presence'; + + private broadcastEnabled = true; + + private lostConTimeout?: NodeJS.Timeout; + + async onNodeDisconnected({ node }: { node: IBrokerNode }): Promise<void> { + const affectedUsers = await this.removeLostConnections(node.id); + return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); + } + + async created(): Promise<void> { + this.onEvent('watch.instanceStatus', async ({ clientAction, id }): Promise<void> => { + if (clientAction !== 'removed') { + return; + } + + const affectedUsers = await this.removeLostConnections(id); + return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); + }); + } + + async started(): Promise<void> { + this.lostConTimeout = setTimeout(async () => { + const affectedUsers = await this.removeLostConnections(); + return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); + }, 10000); + } + + async stopped(): Promise<void> { + if (!this.lostConTimeout) { + return; + } + clearTimeout(this.lostConTimeout); + } + + toggleBroadcast(enabled: boolean): void { + this.broadcastEnabled = enabled; + } + + async newConnection( + uid: string | undefined, + session: string | undefined, + nodeId: string, + ): Promise<{ uid: string; connectionId: string } | undefined> { + if (!uid || !session) { + return; + } + + await UsersSessions.addConnectionById(uid, { + id: session, + instanceId: nodeId, + status: UserStatus.ONLINE, + }); + + await this.updateUserPresence(uid); + return { + uid, + connectionId: session, + }; + } + + async removeConnection(uid: string | undefined, session: string | undefined): Promise<{ uid: string; session: string } | undefined> { + if (!uid || !session) { + return; + } + await UsersSessions.removeConnectionByConnectionId(session); + + await this.updateUserPresence(uid); + + return { + uid, + session, + }; + } + + async removeLostConnections(nodeID?: string): Promise<string[]> { + if (nodeID) { + const affectedUsers = await UsersSessions.findByInstanceId(nodeID).toArray(); + + const { modifiedCount } = await UsersSessions.removeConnectionsFromInstanceId(nodeID); + if (modifiedCount === 0) { + return []; + } + + return affectedUsers.map(({ _id }) => _id); + } + + const nodes = await this.api.nodeList(); + + const ids = nodes.filter((node) => node.available).map(({ id }) => id); + if (ids.length === 0) { + return []; + } + + const affectedUsers = await UsersSessions.findByOtherInstanceIds(ids, { projection: { _id: 1 } }).toArray(); + + const { modifiedCount } = await UsersSessions.removeConnectionsFromOtherInstanceIds(ids); + if (modifiedCount === 0) { + return []; + } + + return affectedUsers.map(({ _id }) => _id); + } + + async setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise<boolean> { + const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] }; + + const user = await Users.findOneById<Pick<IUser, 'username' | 'roles' | 'status'>>(uid, { + projection: { username: 1, roles: 1, status: 1 }, + }); + + const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); + + const result = await Users.updateStatusById(uid, { + statusDefault, + status, + statusConnection, + statusText, + }); + + if (result.modifiedCount > 0) { + this.broadcast({ _id: uid, username: user?.username, status, statusText, roles: user?.roles || [] }, user?.status); + } + + return !!result.modifiedCount; + } + + async setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean> { + const result = await UsersSessions.updateConnectionStatusById(uid, session, status); + + await this.updateUserPresence(uid); + + return !!result.modifiedCount; + } + + async updateUserPresence(uid: string): Promise<void> { + const user = await Users.findOneById<Pick<IUser, 'username' | 'statusDefault' | 'statusText' | 'roles' | 'status'>>(uid, { + projection: { + username: 1, + statusDefault: 1, + statusText: 1, + roles: 1, + status: 1, + }, + }); + if (!user) { + return; + } + + const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] }; + + const { statusDefault } = user; + + const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); + + const result = await Users.updateStatusById(uid, { + status, + statusConnection, + }); + + if (result.modifiedCount > 0) { + this.broadcast({ _id: uid, username: user.username, status, statusText: user.statusText, roles: user.roles }, user.status); + } + } + + private broadcast( + user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'roles'>, + previousStatus: UserStatus | undefined, + ): void { + if (!this.broadcastEnabled) { + return; + } + this.api.broadcast('presence.status', { + user, + previousStatus, + }); + } +} diff --git a/apps/meteor/ee/server/services/presence/lib/processConnectionStatus.ts b/packages/presence/src/lib/processConnectionStatus.ts similarity index 100% rename from apps/meteor/ee/server/services/presence/lib/processConnectionStatus.ts rename to packages/presence/src/lib/processConnectionStatus.ts diff --git a/apps/meteor/ee/tests/unit/server/services/presence/lib/processConnectionStatus.tests.ts b/packages/presence/tests/lib/processConnectionStatus.test.ts similarity index 58% rename from apps/meteor/ee/tests/unit/server/services/presence/lib/processConnectionStatus.tests.ts rename to packages/presence/tests/lib/processConnectionStatus.test.ts index f63c3a2f675..1646636d92b 100644 --- a/apps/meteor/ee/tests/unit/server/services/presence/lib/processConnectionStatus.tests.ts +++ b/packages/presence/tests/lib/processConnectionStatus.test.ts @@ -1,51 +1,47 @@ -import { expect } from 'chai'; +import { describe, expect, test } from '@jest/globals'; import { UserStatus } from '@rocket.chat/core-typings'; -import { - processConnectionStatus, - processStatus, - processPresenceAndStatus, -} from '../../../../../../server/services/presence/lib/processConnectionStatus'; +import { processConnectionStatus, processStatus, processPresenceAndStatus } from '../../src/lib/processConnectionStatus'; describe('Presence micro service', () => { - it('should return connection as online when there is a connection online', () => { - expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.BUSY, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.AWAY, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE); + test('should return connection as online when there is a connection online', () => { + expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); + expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); + expect(processConnectionStatus(UserStatus.BUSY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); + expect(processConnectionStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); }); - it('should return the connections status if the other connection is offline', () => { - expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE); - expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).to.equal(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.AWAY, UserStatus.OFFLINE)).to.equal(UserStatus.AWAY); + test('should return the connections status if the other connection is offline', () => { + expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); + expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.ONLINE); + expect(processConnectionStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.AWAY); }); - it('should return the connection status when the default status is online', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.ONLINE)).to.equal(UserStatus.ONLINE); - expect(processStatus(UserStatus.AWAY, UserStatus.ONLINE)).to.equal(UserStatus.AWAY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).to.equal(UserStatus.OFFLINE); + test('should return the connection status when the default status is online', () => { + expect(processStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); + expect(processStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.AWAY); + expect(processStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.OFFLINE); }); - it('should return status busy when the default status is busy', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.BUSY)).to.equal(UserStatus.BUSY); - expect(processStatus(UserStatus.AWAY, UserStatus.BUSY)).to.equal(UserStatus.BUSY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.BUSY)).to.equal(UserStatus.OFFLINE); + test('should return status busy when the default status is busy', () => { + expect(processStatus(UserStatus.ONLINE, UserStatus.BUSY)).toBe(UserStatus.BUSY); + expect(processStatus(UserStatus.AWAY, UserStatus.BUSY)).toBe(UserStatus.BUSY); + expect(processStatus(UserStatus.OFFLINE, UserStatus.BUSY)).toBe(UserStatus.OFFLINE); }); - it('should return status away when the default status is away', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.AWAY)).to.equal(UserStatus.AWAY); - expect(processStatus(UserStatus.AWAY, UserStatus.AWAY)).to.equal(UserStatus.AWAY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.AWAY)).to.equal(UserStatus.OFFLINE); + test('should return status away when the default status is away', () => { + expect(processStatus(UserStatus.ONLINE, UserStatus.AWAY)).toBe(UserStatus.AWAY); + expect(processStatus(UserStatus.AWAY, UserStatus.AWAY)).toBe(UserStatus.AWAY); + expect(processStatus(UserStatus.OFFLINE, UserStatus.AWAY)).toBe(UserStatus.OFFLINE); }); - it('should return status offline when the default status is offline', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE); - expect(processStatus(UserStatus.AWAY, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE); - expect(processStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).to.equal(UserStatus.OFFLINE); + test('should return status offline when the default status is offline', () => { + expect(processStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); + expect(processStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); + expect(processStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); }); - it('should return correct status and statusConnection when connected once', () => { + test('should return correct status and statusConnection when connected once', () => { expect( processPresenceAndStatus( [ @@ -59,7 +55,7 @@ describe('Presence micro service', () => { ], UserStatus.ONLINE, ), - ).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -74,7 +70,7 @@ describe('Presence micro service', () => { ], UserStatus.ONLINE, ), - ).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); + ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); expect( processPresenceAndStatus( @@ -89,7 +85,7 @@ describe('Presence micro service', () => { ], UserStatus.BUSY, ), - ).to.deep.equal({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -104,7 +100,7 @@ describe('Presence micro service', () => { ], UserStatus.AWAY, ), - ).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -119,7 +115,7 @@ describe('Presence micro service', () => { ], UserStatus.BUSY, ), - ).to.deep.equal({ status: UserStatus.BUSY, statusConnection: UserStatus.AWAY }); + ).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.AWAY }); expect( processPresenceAndStatus( @@ -134,7 +130,7 @@ describe('Presence micro service', () => { ], UserStatus.OFFLINE, ), - ).to.deep.equal({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -149,10 +145,10 @@ describe('Presence micro service', () => { ], UserStatus.OFFLINE, ), - ).to.deep.equal({ status: UserStatus.OFFLINE, statusConnection: UserStatus.AWAY }); + ).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.AWAY }); }); - it('should return correct status and statusConnection when connected twice', () => { + test('should return correct status and statusConnection when connected twice', () => { expect( processPresenceAndStatus( [ @@ -173,7 +169,7 @@ describe('Presence micro service', () => { ], UserStatus.ONLINE, ), - ).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -195,7 +191,7 @@ describe('Presence micro service', () => { ], UserStatus.ONLINE, ), - ).to.deep.equal({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); + ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); expect( processPresenceAndStatus( @@ -217,26 +213,26 @@ describe('Presence micro service', () => { ], UserStatus.ONLINE, ), - ).to.deep.equal({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); + ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); }); - it('should return correct status and statusConnection when not connected', () => { - expect(processPresenceAndStatus([], UserStatus.ONLINE)).to.deep.equal({ + test('should return correct status and statusConnection when not connected', () => { + expect(processPresenceAndStatus([], UserStatus.ONLINE)).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE, }); - expect(processPresenceAndStatus([], UserStatus.BUSY)).to.deep.equal({ + expect(processPresenceAndStatus([], UserStatus.BUSY)).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE, }); - expect(processPresenceAndStatus([], UserStatus.AWAY)).to.deep.equal({ + expect(processPresenceAndStatus([], UserStatus.AWAY)).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE, }); - expect(processPresenceAndStatus([], UserStatus.OFFLINE)).to.deep.equal({ + expect(processPresenceAndStatus([], UserStatus.OFFLINE)).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE, }); diff --git a/packages/presence/tsconfig.json b/packages/presence/tsconfig.json new file mode 100644 index 00000000000..d5d962aae2e --- /dev/null +++ b/packages/presence/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + // "declaration": false, + "declarationMap": false + }, + "include": ["../../apps/meteor/definition/externals/meteor/rocketchat-streamer.d.ts"], + "exclude": ["./dist"], + "files": ["./src/Presence.ts"] +} diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index ca42e250f5d..54c69184505 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,4 +1,4 @@ -import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken } from '@rocket.chat/core-typings'; +import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken, UserStatus } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST'; @@ -268,7 +268,7 @@ export type UsersEndpoints = { }; '/v1/users.setStatus': { - POST: (params: { message?: string; status?: 'online' | 'offline' | 'away' | 'busy' }) => void; + POST: (params: { message?: string; status?: UserStatus }) => void; }; '/v1/users.getStatus': { diff --git a/turbo.json b/turbo.json index c6f4725630b..0ea3c386cea 100644 --- a/turbo.json +++ b/turbo.json @@ -36,6 +36,9 @@ "dependsOn": ["build"], "cache": false }, + "ms": { + "dependsOn": ["^build"] + }, "@rocket.chat/ui-contexts#build": { "dependsOn": ["^build"], "cache": false diff --git a/yarn.lock b/yarn.lock index f6b742011b7..0e96d0f2727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,6 +28,13 @@ __metadata: languageName: node linkType: hard +"@arr/every@npm:^1.0.0": + version: 1.0.1 + resolution: "@arr/every@npm:1.0.1" + checksum: fc33cd8f3244db7053b89898fc0d1753dd17f40328a443d8640dc15578ed74c59d8e12fe8de87ddb20a2cee578dc0dd9fbb94eb77ef209158fb615bb904b684f + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -53,6 +60,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/compat-data@npm:7.19.1" + checksum: f985887ea08a140e4af87a94d3fb17af0345491eb97f5a85b1840255c2e2a97429f32a8fd12a7aae9218af5f1024f1eb12a5cd280d2d69b2337583c17ea506ba + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -100,6 +114,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/core@npm:7.19.1" + dependencies: + "@ampproject/remapping": ^2.1.0 + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.19.0 + "@babel/helper-compilation-targets": ^7.19.1 + "@babel/helper-module-transforms": ^7.19.0 + "@babel/helpers": ^7.19.0 + "@babel/parser": ^7.19.1 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.1 + "@babel/types": ^7.19.0 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.1 + semver: ^6.3.0 + checksum: 941c8c119b80bdba5fafc80bbaa424d51146b6d3c30b8fae35879358dd37c11d3d0926bc7e970a0861229656eedaa8c884d4a3a25cc904086eb73b827a2f1168 + languageName: node + linkType: hard + "@babel/eslint-parser@npm:^7.18.9": version: 7.18.9 resolution: "@babel/eslint-parser@npm:7.18.9" @@ -125,6 +162,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/generator@npm:7.19.0" + dependencies: + "@babel/types": ^7.19.0 + "@jridgewell/gen-mapping": ^0.3.2 + jsesc: ^2.5.1 + checksum: aa3d5785cf8f8e81672dcc61aef351188efeadb20d9f66d79113d82cbcf3bbbdeb829989fa14582108572ddbc4e4027bdceb06ccaf5ec40fa93c2dda8fbcd4aa + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" @@ -158,6 +206,20 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.19.0, @babel/helper-compilation-targets@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/helper-compilation-targets@npm:7.19.1" + dependencies: + "@babel/compat-data": ^7.19.1 + "@babel/helper-validator-option": ^7.18.6 + browserslist: ^4.21.3 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: c2d3039265e498b341a6b597f855f2fcef02659050fefedf36ad4e6815e6aafe1011a761214cc80d98260ed07ab15a8cbe959a0458e97bec5f05a450e1b1741b + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.16.7, @babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-create-class-features-plugin@npm:7.18.6" @@ -175,6 +237,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-create-class-features-plugin@npm:7.19.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-member-expression-to-functions": ^7.18.9 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/helper-replace-supers": ^7.18.9 + "@babel/helper-split-export-declaration": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: f0c6fb77b6f113d70f308e7093f60dd465b697818badf5df0519d8dd12b6bfb1f4ad300b923207ce9f9c1c940ef58bff12ac4270c0863eadf9e303b7dd6d01b6 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.18.6" @@ -187,6 +266,18 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-regexp-features-plugin@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.19.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + regexpu-core: ^5.1.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 811cc90afe9fc25a74ed37fc0c1361a4a91b0b940235dd3958e3f03b366d40a903b40fc93b51bcb93be774aba573219f8f215664bea1d1301f58797ca6854f3f + languageName: node + linkType: hard + "@babel/helper-define-polyfill-provider@npm:^0.1.5": version: 0.1.5 resolution: "@babel/helper-define-polyfill-provider@npm:0.1.5" @@ -223,6 +314,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-define-polyfill-provider@npm:^0.3.3": + version: 0.3.3 + resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3" + dependencies: + "@babel/helper-compilation-targets": ^7.17.7 + "@babel/helper-plugin-utils": ^7.16.7 + debug: ^4.1.1 + lodash.debounce: ^4.0.8 + resolve: ^1.14.2 + semver: ^6.1.2 + peerDependencies: + "@babel/core": ^7.4.0-0 + checksum: 8e3fe75513302e34f6d92bd67b53890e8545e6c5bca8fe757b9979f09d68d7e259f6daea90dc9e01e332c4f8781bda31c5fe551c82a277f9bc0bec007aed497c + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.18.6, @babel/helper-environment-visitor@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-environment-visitor@npm:7.18.9" @@ -249,6 +356,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-function-name@npm:7.19.0" + dependencies: + "@babel/template": ^7.18.10 + "@babel/types": ^7.19.0 + checksum: eac1f5db428ba546270c2b8d750c24eb528b8fcfe50c81de2e0bdebf0e20f24bec688d4331533b782e4a907fad435244621ca2193cfcf80a86731299840e0f6e + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-hoist-variables@npm:7.18.6" @@ -292,6 +409,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-module-transforms@npm:7.19.0" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-module-imports": ^7.18.6 + "@babel/helper-simple-access": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/helper-validator-identifier": ^7.18.6 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.0 + "@babel/types": ^7.19.0 + checksum: 4483276c66f56cf3b5b063634092ad9438c2593725de5c143ba277dda82f1501e6d73b311c1b28036f181dbe36eaeff29f24726cde37a599d4e735af294e5359 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" @@ -315,6 +448,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helper-plugin-utils@npm:7.19.0" + checksum: eedc996c633c8c207921c26ec2989eae0976336ecd9b9f1ac526498f52b5d136f7cd03c32b6fdf8d46a426f907c142de28592f383c42e5fba1e904cbffa05345 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-remap-async-to-generator@npm:7.18.6" @@ -329,6 +469,20 @@ __metadata: languageName: node linkType: hard +"@babel/helper-remap-async-to-generator@npm:^7.18.9": + version: 7.18.9 + resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-wrap-function": ^7.18.9 + "@babel/types": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 4be6076192308671b046245899b703ba090dbe7ad03e0bea897bb2944ae5b88e5e85853c9d1f83f643474b54c578d8ac0800b80341a86e8538264a725fbbefec + languageName: node + linkType: hard + "@babel/helper-replace-supers@npm:^7.16.7, @babel/helper-replace-supers@npm:^7.18.6, @babel/helper-replace-supers@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-replace-supers@npm:7.18.9" @@ -369,6 +523,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/helper-string-parser@npm:7.18.10" + checksum: d554a4393365b624916b5c00a4cc21c990c6617e7f3fe30be7d9731f107f12c33229a7a3db9d829bfa110d2eb9f04790745d421640e3bd245bb412dc0ea123c1 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-validator-identifier@npm:7.18.6" @@ -395,6 +556,18 @@ __metadata: languageName: node linkType: hard +"@babel/helper-wrap-function@npm:^7.18.9": + version: 7.19.0 + resolution: "@babel/helper-wrap-function@npm:7.19.0" + dependencies: + "@babel/helper-function-name": ^7.19.0 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.0 + "@babel/types": ^7.19.0 + checksum: 2453a6b134f12cc779179188c4358a66252c29b634a8195c0cf626e17f9806c3c4c40e159cd8056c2ec82b69b9056a088014fa43d6ccc1aca67da8d9605da8fd + languageName: node + linkType: hard + "@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helpers@npm:7.18.9" @@ -406,6 +579,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/helpers@npm:7.19.0" + dependencies: + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.19.0 + "@babel/types": ^7.19.0 + checksum: e50e78e0dbb0435075fa3f85021a6bcae529589800bca0292721afd7f7c874bea54508d6dc57eca16e5b8224f8142c6b0e32e3b0140029dc09865da747da4623 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.18.6": version: 7.18.6 resolution: "@babel/highlight@npm:7.18.6" @@ -426,6 +610,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/parser@npm:7.19.1" + bin: + parser: ./bin/babel-parser.js + checksum: b1e0acb346b2a533c857e1e97ac0886cdcbd76aafef67835a2b23f760c10568eb53ad8a27dd5f862d8ba4e583742e6067f107281ccbd68959d61bc61e4ddaa51 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -464,6 +657,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-async-generator-functions@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.19.1" + dependencies: + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-remap-async-to-generator": ^7.18.9 + "@babel/plugin-syntax-async-generators": ^7.8.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f101555b00aee6ee0107c9e40d872ad646bbd3094abdbeda56d17b107df69a0cb49e5d02dcf5f9d8753e25564e798d08429f12d811aaa1b307b6a725c0b8159c + languageName: node + linkType: hard + "@babel/plugin-proposal-class-properties@npm:^7.12.1, @babel/plugin-proposal-class-properties@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" @@ -822,7 +1029,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.18.6": +"@babel/plugin-syntax-jsx@npm:^7.18.6, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.18.6 resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" dependencies: @@ -932,6 +1139,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-syntax-typescript@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2cde73725ec51118ebf410bf02d78781c03fa4d3185993fcc9d253b97443381b621c44810084c5dd68b92eb8bdfae0e5b163e91b32bebbb33852383d1815c05d + languageName: node + linkType: hard + "@babel/plugin-transform-arrow-functions@npm:^7.12.1, @babel/plugin-transform-arrow-functions@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-arrow-functions@npm:7.18.6" @@ -996,6 +1214,25 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/plugin-transform-classes@npm:7.19.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.18.6 + "@babel/helper-compilation-targets": ^7.19.0 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-optimise-call-expression": ^7.18.6 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-replace-supers": ^7.18.9 + "@babel/helper-split-export-declaration": ^7.18.6 + globals: ^11.1.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5500953031fc3eae73f717c7b59ef406158a4a710d566a0f78a4944240bcf98f817f07cf1d6af0e749e21f0dfee29c36412b75d57b0a753c3ad823b70c596b79 + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-computed-properties@npm:7.18.9" @@ -1018,6 +1255,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.18.13": + version: 7.18.13 + resolution: "@babel/plugin-transform-destructuring@npm:7.18.13" + dependencies: + "@babel/helper-plugin-utils": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 83e44ec93a4cfbf69376db8836d00ec803820081bf0f8b6cea73a9b3cd320b8285768d5b385744af4a27edda4b6502245c52d3ed026ea61356faf57bfe78effb + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.18.6, @babel/plugin-transform-dotall-regex@npm:^7.4.4": version: 7.18.6 resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6" @@ -1153,6 +1401,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-systemjs@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.19.0" + dependencies: + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-module-transforms": ^7.19.0 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-validator-identifier": ^7.18.6 + babel-plugin-dynamic-import-node: ^2.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a0742deee4a076d6fc303d036c1ea2bea9b7d91af390483fe91fc415f9cb43925bb5dd930fdcb8fcdc9d4c7a22774a3cec521c67f1422a9b473debcb85ee57f9 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-umd@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-modules-umd@npm:7.18.6" @@ -1177,6 +1440,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.19.1" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.19.0 + "@babel/helper-plugin-utils": ^7.19.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 8a40f5d04f2140c44fe890a5a3fd72abc2a88445443ac2bd92e1e85d9366d3eb8f1ebb7e2c89d2daeaf213d9b28cb65605502ac9b155936d48045eeda6053494 + languageName: node + linkType: hard + "@babel/plugin-transform-new-target@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-new-target@npm:7.18.6" @@ -1317,6 +1592,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-spread@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/plugin-transform-spread@npm:7.19.0" + dependencies: + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e73a4deb095999185e70b524d0ff4e35df50fcda58299e700a6149a15bbc1a9b369ef1cef384e15a54b3c3ce316cc0f054dbf249dcd0d1ca59f4281dd4df9718 + languageName: node + linkType: hard + "@babel/plugin-transform-sticky-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-sticky-regex@npm:7.18.6" @@ -1363,6 +1650,30 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.18.6": + version: 7.19.1 + resolution: "@babel/plugin-transform-typescript@npm:7.19.1" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.19.0 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/plugin-syntax-typescript": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 434752f9cfb3cfe5dc0a3c8118b404bb7340b665c01cf6b817a9d6dafa10ca128fccecf4c507286fb00a92b89bcabeb8256e67c18aef5db9fdc4eb8a71881d70 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-escapes@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.10" + dependencies: + "@babel/helper-plugin-utils": ^7.18.9 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f5baca55cb3c11bc08ec589f5f522d85c1ab509b4d11492437e45027d64ae0b22f0907bd1381e8d7f2a436384bb1f9ad89d19277314242c5c2671a0f91d0f9cd + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.6" @@ -1471,6 +1782,91 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/preset-env@npm:7.19.1" + dependencies: + "@babel/compat-data": ^7.19.1 + "@babel/helper-compilation-targets": ^7.19.1 + "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-async-generator-functions": ^7.19.1 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-class-static-block": ^7.18.6 + "@babel/plugin-proposal-dynamic-import": ^7.18.6 + "@babel/plugin-proposal-export-namespace-from": ^7.18.9 + "@babel/plugin-proposal-json-strings": ^7.18.6 + "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 + "@babel/plugin-proposal-numeric-separator": ^7.18.6 + "@babel/plugin-proposal-object-rest-spread": ^7.18.9 + "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 + "@babel/plugin-proposal-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-private-methods": ^7.18.6 + "@babel/plugin-proposal-private-property-in-object": ^7.18.6 + "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.18.6 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.18.6 + "@babel/plugin-transform-async-to-generator": ^7.18.6 + "@babel/plugin-transform-block-scoped-functions": ^7.18.6 + "@babel/plugin-transform-block-scoping": ^7.18.9 + "@babel/plugin-transform-classes": ^7.19.0 + "@babel/plugin-transform-computed-properties": ^7.18.9 + "@babel/plugin-transform-destructuring": ^7.18.13 + "@babel/plugin-transform-dotall-regex": ^7.18.6 + "@babel/plugin-transform-duplicate-keys": ^7.18.9 + "@babel/plugin-transform-exponentiation-operator": ^7.18.6 + "@babel/plugin-transform-for-of": ^7.18.8 + "@babel/plugin-transform-function-name": ^7.18.9 + "@babel/plugin-transform-literals": ^7.18.9 + "@babel/plugin-transform-member-expression-literals": ^7.18.6 + "@babel/plugin-transform-modules-amd": ^7.18.6 + "@babel/plugin-transform-modules-commonjs": ^7.18.6 + "@babel/plugin-transform-modules-systemjs": ^7.19.0 + "@babel/plugin-transform-modules-umd": ^7.18.6 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.19.1 + "@babel/plugin-transform-new-target": ^7.18.6 + "@babel/plugin-transform-object-super": ^7.18.6 + "@babel/plugin-transform-parameters": ^7.18.8 + "@babel/plugin-transform-property-literals": ^7.18.6 + "@babel/plugin-transform-regenerator": ^7.18.6 + "@babel/plugin-transform-reserved-words": ^7.18.6 + "@babel/plugin-transform-shorthand-properties": ^7.18.6 + "@babel/plugin-transform-spread": ^7.19.0 + "@babel/plugin-transform-sticky-regex": ^7.18.6 + "@babel/plugin-transform-template-literals": ^7.18.9 + "@babel/plugin-transform-typeof-symbol": ^7.18.9 + "@babel/plugin-transform-unicode-escapes": ^7.18.10 + "@babel/plugin-transform-unicode-regex": ^7.18.6 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.19.0 + babel-plugin-polyfill-corejs2: ^0.3.3 + babel-plugin-polyfill-corejs3: ^0.6.0 + babel-plugin-polyfill-regenerator: ^0.4.1 + core-js-compat: ^3.25.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3fcd4f3e768b8b0c9e8f9fb2b23d694d838d3cc936c783aaa9c436b863ae24811059b6ffed80e2ac7d54e7d2c18b0a190f4de05298cf461d27b2817b617ea71f + languageName: node + linkType: hard + "@babel/preset-flow@npm:^7.12.1": version: 7.16.7 resolution: "@babel/preset-flow@npm:7.16.7" @@ -1528,6 +1924,19 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/preset-typescript@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-transform-typescript": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7fe0da5103eb72d3cf39cf3e138a794c8cdd19c0b38e3e101507eef519c46a87a0d6d0e8bc9e28a13ea2364001ebe7430b9d75758aab4c3c3a8db9a487b9dc7c + languageName: node + linkType: hard + "@babel/register@npm:^7.12.1, @babel/register@npm:^7.18.9": version: 7.18.9 resolution: "@babel/register@npm:7.18.9" @@ -1599,6 +2008,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/template@npm:7.18.10" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/parser": ^7.18.10 + "@babel/types": ^7.18.10 + checksum: 93a6aa094af5f355a72bd55f67fa1828a046c70e46f01b1606e6118fa1802b6df535ca06be83cc5a5e834022be95c7b714f0a268b5f20af984465a71e28f1473 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.18.6, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.7.2": version: 7.18.9 resolution: "@babel/traverse@npm:7.18.9" @@ -1617,6 +2037,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/traverse@npm:7.19.1" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.19.0 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.19.1 + "@babel/types": ^7.19.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 9d782b5089ebc989e54c2406814ed1206cb745ed2734e6602dee3e23d4b6ebbb703ff86e536276630f8de83fda6cde99f0634e3c3d847ddb40572d0303ba8800 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.18.9 resolution: "@babel/types@npm:7.18.9" @@ -1627,6 +2065,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.18.10, @babel/types@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/types@npm:7.19.0" + dependencies: + "@babel/helper-string-parser": ^7.18.10 + "@babel/helper-validator-identifier": ^7.18.6 + to-fast-properties: ^2.0.0 + checksum: 9b346715a68aeede70ba9c685a144b0b26c53bcd595d448e24c8fa8df4d5956a5712e56ebadb7c85dcc32f218ee42788e37b93d50d3295c992072224cb3ef3fe + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -2211,6 +2660,20 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/console@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + "@types/node": "*" + chalk: ^4.0.0 + jest-message-util: ^29.0.3 + jest-util: ^29.0.3 + slash: ^3.0.0 + checksum: 1c5f092082c45c5c35ea51e7c75f4ce06a9b4350e44e0d3aa6c586c469a687fb095ab2601f196f147cccd1c4b5cbf4adc885ae83c8af42dfeee5aa518fa0968e + languageName: node + linkType: hard + "@jest/core@npm:^27.5.1": version: 27.5.1 resolution: "@jest/core@npm:27.5.1" @@ -2252,6 +2715,47 @@ __metadata: languageName: node linkType: hard +"@jest/core@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/core@npm:29.0.3" + dependencies: + "@jest/console": ^29.0.3 + "@jest/reporters": ^29.0.3 + "@jest/test-result": ^29.0.3 + "@jest/transform": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + ci-info: ^3.2.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-changed-files: ^29.0.0 + jest-config: ^29.0.3 + jest-haste-map: ^29.0.3 + jest-message-util: ^29.0.3 + jest-regex-util: ^29.0.0 + jest-resolve: ^29.0.3 + jest-resolve-dependencies: ^29.0.3 + jest-runner: ^29.0.3 + jest-runtime: ^29.0.3 + jest-snapshot: ^29.0.3 + jest-util: ^29.0.3 + jest-validate: ^29.0.3 + jest-watcher: ^29.0.3 + micromatch: ^4.0.4 + pretty-format: ^29.0.3 + slash: ^3.0.0 + strip-ansi: ^6.0.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 411a994ae0df96262c911c894b231a3148280ae39cf0a5d1132c126e708925a3aa89d3f75be628260916a5c38c6ee1ce27979cf78171a393f3c3bbac019149d9 + languageName: node + linkType: hard + "@jest/environment@npm:^27.5.1": version: 27.5.1 resolution: "@jest/environment@npm:27.5.1" @@ -2264,6 +2768,37 @@ __metadata: languageName: node linkType: hard +"@jest/environment@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/environment@npm:29.0.3" + dependencies: + "@jest/fake-timers": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + jest-mock: ^29.0.3 + checksum: 3cf9a6c18d1175f9d9dc353ad26a8482cef3aae8d68574d2c2feaf149e4d6f5c83e145aeefffdc0c614e9b770d26251e476cb1bd86f140c9d19b6adf8f1a2681 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/expect-utils@npm:29.0.3" + dependencies: + jest-get-type: ^29.0.0 + checksum: af6fa6e0b9cdf42f5778ff0b70c2049ec768598f720ea473773e0c0bebd2416a32ecbede94cfdc95572a021eda5302a9295a5c416ad5ce155c4ec277c40129da + languageName: node + linkType: hard + +"@jest/expect@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/expect@npm:29.0.3" + dependencies: + expect: ^29.0.3 + jest-snapshot: ^29.0.3 + checksum: 8f969cce260b84edc105a73b8314accd305f2ad012031c00a6a4ba8b3db864237719e95a167702badada274bd764c306e561326bef86d950f72b94f5c9c69c7e + languageName: node + linkType: hard + "@jest/fake-timers@npm:^27.5.1": version: 27.5.1 resolution: "@jest/fake-timers@npm:27.5.1" @@ -2278,6 +2813,20 @@ __metadata: languageName: node linkType: hard +"@jest/fake-timers@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/fake-timers@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + "@sinonjs/fake-timers": ^9.1.2 + "@types/node": "*" + jest-message-util: ^29.0.3 + jest-mock: ^29.0.3 + jest-util: ^29.0.3 + checksum: c0a641fe239044a766eb27e6e4e085acdc8f53d34813aa883a8da8fcce555d8b6ce06716b94c72b44e60c0ca8088b1f1a1d3b05c7f41ed39fc0f6cf23dead7c4 + languageName: node + linkType: hard + "@jest/globals@npm:^27.5.1": version: 27.5.1 resolution: "@jest/globals@npm:27.5.1" @@ -2289,6 +2838,18 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/globals@npm:29.0.3" + dependencies: + "@jest/environment": ^29.0.3 + "@jest/expect": ^29.0.3 + "@jest/types": ^29.0.3 + jest-mock: ^29.0.3 + checksum: ab6a3f93b98c600f6b4d57c5cf593e624847101bf037f452800d891f55612cd042f524f032f4871ff784c1814f85a4939afbd853104f50f78aada353ac124b7e + languageName: node + linkType: hard + "@jest/reporters@npm:^27.5.1": version: 27.5.1 resolution: "@jest/reporters@npm:27.5.1" @@ -2327,6 +2888,44 @@ __metadata: languageName: node linkType: hard +"@jest/reporters@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/reporters@npm:29.0.3" + dependencies: + "@bcoe/v8-coverage": ^0.2.3 + "@jest/console": ^29.0.3 + "@jest/test-result": ^29.0.3 + "@jest/transform": ^29.0.3 + "@jest/types": ^29.0.3 + "@jridgewell/trace-mapping": ^0.3.15 + "@types/node": "*" + chalk: ^4.0.0 + collect-v8-coverage: ^1.0.0 + exit: ^0.1.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + istanbul-lib-coverage: ^3.0.0 + istanbul-lib-instrument: ^5.1.0 + istanbul-lib-report: ^3.0.0 + istanbul-lib-source-maps: ^4.0.0 + istanbul-reports: ^3.1.3 + jest-message-util: ^29.0.3 + jest-util: ^29.0.3 + jest-worker: ^29.0.3 + slash: ^3.0.0 + string-length: ^4.0.1 + strip-ansi: ^6.0.0 + terminal-link: ^2.0.0 + v8-to-istanbul: ^9.0.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 43028a8823cb8d58c5219e0471990e0d7e9014ed9ef6f2853076bd9e49a490fc1bcfcf46a4d7b981725da0f31be1496725a4a0edb0149f7c7f54c5d2299dcae1 + languageName: node + linkType: hard + "@jest/schemas@npm:^28.0.2": version: 28.0.2 resolution: "@jest/schemas@npm:28.0.2" @@ -2336,6 +2935,15 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:^29.0.0": + version: 29.0.0 + resolution: "@jest/schemas@npm:29.0.0" + dependencies: + "@sinclair/typebox": ^0.24.1 + checksum: 41355c78f09eb1097e57a3c5d0ca11c9099e235e01ea5fa4e3953562a79a6a9296c1d300f1ba50ca75236048829e056b00685cd2f1ff8285e56fd2ce01249acb + languageName: node + linkType: hard + "@jest/source-map@npm:^27.5.1": version: 27.5.1 resolution: "@jest/source-map@npm:27.5.1" @@ -2347,6 +2955,17 @@ __metadata: languageName: node linkType: hard +"@jest/source-map@npm:^29.0.0": + version: 29.0.0 + resolution: "@jest/source-map@npm:29.0.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.15 + callsites: ^3.0.0 + graceful-fs: ^4.2.9 + checksum: dd97bc5826cf68d6eb5565383816332f800476232fd12800bd027a259cbf3ef216f1633405f3ad0861dde3b12a7886301798c078b334f6d3012044d43abcf4f6 + languageName: node + linkType: hard + "@jest/test-result@npm:^27.5.1": version: 27.5.1 resolution: "@jest/test-result@npm:27.5.1" @@ -2359,6 +2978,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-result@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/test-result@npm:29.0.3" + dependencies: + "@jest/console": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/istanbul-lib-coverage": ^2.0.0 + collect-v8-coverage: ^1.0.0 + checksum: 9cb76090b2b49cc19f95c51e3593085ab88b2d9539f9c15b1e7919f770aaee75376b453f30a14f2034a5cb25fa8e14f5fcc422f05954dbdb0873220576d9c9a0 + languageName: node + linkType: hard + "@jest/test-sequencer@npm:^27.5.1": version: 27.5.1 resolution: "@jest/test-sequencer@npm:27.5.1" @@ -2371,6 +3002,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-sequencer@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/test-sequencer@npm:29.0.3" + dependencies: + "@jest/test-result": ^29.0.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.0.3 + slash: ^3.0.0 + checksum: c6868e29a36c2dd4f6aa71a7148fa7bf34fb845e97b29bc418cb6988245ad7f0cd820aa6dcf9389d0e5b4592b9df8a727a40b0b91eee0dad00f683c250b94a2c + languageName: node + linkType: hard + "@jest/transform@npm:^26.6.2": version: 26.6.2 resolution: "@jest/transform@npm:26.6.2" @@ -2417,6 +3060,29 @@ __metadata: languageName: node linkType: hard +"@jest/transform@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/transform@npm:29.0.3" + dependencies: + "@babel/core": ^7.11.6 + "@jest/types": ^29.0.3 + "@jridgewell/trace-mapping": ^0.3.15 + babel-plugin-istanbul: ^6.1.1 + chalk: ^4.0.0 + convert-source-map: ^1.4.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.0.3 + jest-regex-util: ^29.0.0 + jest-util: ^29.0.3 + micromatch: ^4.0.4 + pirates: ^4.0.4 + slash: ^3.0.0 + write-file-atomic: ^4.0.1 + checksum: c68ebb673a27640372c912736aa26bda5bc4dfd7a890bb10c467b81e8a66826c8b8b6826ebf25ed3c7a70b7818fcc60e3c0d7341d1595d5ce4978d53d22a7ea1 + languageName: node + linkType: hard + "@jest/types@npm:^26.6.2": version: 26.6.2 resolution: "@jest/types@npm:26.6.2" @@ -2443,6 +3109,20 @@ __metadata: languageName: node linkType: hard +"@jest/types@npm:^29.0.3": + version: 29.0.3 + resolution: "@jest/types@npm:29.0.3" + dependencies: + "@jest/schemas": ^29.0.0 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: 3bd33e64d87a5421b860396ac7f7b9b8d5abbf0f300f4379bb20c8e3a6169fbbd078933ce0649827cd63e23330c4effeb6b222fa94e8dd0df638dfff6c1fed41 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": version: 0.3.2 resolution: "@jridgewell/gen-mapping@npm:0.3.2" @@ -2505,7 +3185,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.14": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.15": version: 0.3.15 resolution: "@jridgewell/trace-mapping@npm:0.3.15" dependencies: @@ -3184,6 +3864,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^0.5.0": + version: 0.5.0 + resolution: "@polka/url@npm:0.5.0" + checksum: 3f007adf9c271b28992ebff1df6424e75e7d579493c66969356a9b5dada18480583744dbc28a7467371fa10eb794a5e1dc1f3fcd359c0b5685f4f9c6592cd312 + languageName: node + linkType: hard + "@react-aria/breadcrumbs@npm:^3.3.1": version: 3.3.1 resolution: "@react-aria/breadcrumbs@npm:3.3.1" @@ -4837,6 +5524,7 @@ __metadata: pino: ^7.11.0 pino-pretty: ^7.6.1 sharp: ^0.30.7 + ts-node: ^10.9.1 typescript: ~4.5.5 underscore: ^1.13.4 uuid: ^7.0.3 @@ -4845,9 +5533,9 @@ __metadata: linkType: soft "@rocket.chat/emitter@npm:next": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/emitter@npm:0.31.19-dev.19" - checksum: 1c87c6007779d3253da57bf7917d02c645fa9d9f90eb2b4748fd8345d87d2555b81474ae33720a1f52504eecaa777c6bd8eef3a8348d2bdf8821463164472045 + version: 0.31.19-dev.7 + resolution: "@rocket.chat/emitter@npm:0.31.19-dev.7" + checksum: 328b0c061a831efe66a66f36189a9039ff5bfb9dd95d2a6d63cdc89798b8abd8f9de786b041b261c913c84190c410e39a2304cd909bc45f619e67ba82407c522 languageName: node linkType: hard @@ -5324,6 +6012,7 @@ __metadata: "@rocket.chat/mp3-encoder": 0.24.0 "@rocket.chat/onboarding-ui": next "@rocket.chat/poplib": "workspace:^" + "@rocket.chat/presence": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": next "@rocket.chat/ui-client": "workspace:^" @@ -5576,7 +6265,7 @@ __metadata: tar-stream: ^1.6.2 template-file: ^6.0.1 tinykeys: ^1.4.0 - ts-node: ^10.8.1 + ts-node: ^10.9.1 turndown: ^7.1.1 twilio: ^3.76.1 twit: ^2.2.11 @@ -5670,6 +6359,53 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/presence-service@workspace:ee/apps/presence-service": + version: 0.0.0-use.local + resolution: "@rocket.chat/presence-service@workspace:ee/apps/presence-service" + dependencies: + "@rocket.chat/emitter": next + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/presence": "workspace:^" + "@rocket.chat/string-helpers": next + "@types/eslint": ^8 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + eslint: ^8.21.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.1 + moleculer: ^0.14.21 + mongodb: ^4.3.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + +"@rocket.chat/presence@workspace:^, @rocket.chat/presence@workspace:packages/presence": + version: 0.0.0-use.local + resolution: "@rocket.chat/presence@workspace:packages/presence" + dependencies: + "@babel/core": ^7.19.1 + "@babel/preset-env": ^7.19.1 + "@babel/preset-typescript": ^7.18.6 + "@rocket.chat/apps-engine": ^1.32.0 + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" + "@types/node": ^14.18.21 + babel-jest: ^29.0.3 + eslint: ^8.21.0 + jest: ^29.0.3 + mongodb: ^4.3.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/prettier-config@npm:next": version: 0.31.17-dev.32 resolution: "@rocket.chat/prettier-config@npm:0.31.17-dev.32" @@ -5882,6 +6618,13 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.24.1": + version: 0.24.41 + resolution: "@sinclair/typebox@npm:0.24.41" + checksum: eb9861ad7bc5a29d5a6be27732757210edfcfa73fca386e303b0363af31c7ad16ebad75cf0c3fdf6444663dda5884ba0de333fc7a8ab8680c1c01e1e91089c1d + languageName: node + linkType: hard + "@sindresorhus/is@npm:^0.7.0": version: 0.7.0 resolution: "@sindresorhus/is@npm:0.7.0" @@ -7889,6 +8632,17 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:*": + version: 4.17.31 + resolution: "@types/express-serve-static-core@npm:4.17.31" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + checksum: 009bfbe1070837454a1056aa710d0390ee5fb8c05dfe5a1691cc3e2ca88dc256f80e1ca27cb51a978681631d2f6431bfc9ec352ea46dd0c6eb183d0170bde5df + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.18": version: 4.17.28 resolution: "@types/express-serve-static-core@npm:4.17.28" @@ -7936,7 +8690,7 @@ __metadata: languageName: node linkType: hard -"@types/graceful-fs@npm:^4.1.2": +"@types/graceful-fs@npm:^4.1.2, @types/graceful-fs@npm:^4.1.3": version: 4.1.5 resolution: "@types/graceful-fs@npm:4.1.5" dependencies: @@ -8382,6 +9136,18 @@ __metadata: languageName: node linkType: hard +"@types/polka@npm:^0.5.4": + version: 0.5.4 + resolution: "@types/polka@npm:0.5.4" + dependencies: + "@types/express": "*" + "@types/express-serve-static-core": "*" + "@types/node": "*" + "@types/trouter": "*" + checksum: 8027a6cf6989d63ad01346c031698885586f3b800746bc35c84ab66f981e452cc58f8770fec703509cacf615f450c7010eb5221cef47a75e9494cdc42df8f8b3 + languageName: node + linkType: hard + "@types/postcss-url@npm:^10": version: 10.0.0 resolution: "@types/postcss-url@npm:10.0.0" @@ -8689,6 +9455,13 @@ __metadata: languageName: node linkType: hard +"@types/trouter@npm:*": + version: 3.1.1 + resolution: "@types/trouter@npm:3.1.1" + checksum: 773417a0ba388e4e95529a576a921d14be1656eb8f79f7be85ce6b4537733b98ac40edb6de53e0fe11a341e4354a2336698336421badb454c99718a90b9fdc03 + languageName: node + linkType: hard + "@types/trusted-types@npm:*": version: 2.0.2 resolution: "@types/trusted-types@npm:2.0.2" @@ -8858,6 +9631,15 @@ __metadata: languageName: node linkType: hard +"@types/yargs@npm:^17.0.8": + version: 17.0.12 + resolution: "@types/yargs@npm:17.0.12" + dependencies: + "@types/yargs-parser": "*" + checksum: 5b41d21d8624199f89db82209b2adab2e47867b3677e852fde65698be2ca48364b14c2e70cb0adc9bca4a2102c93dad2409cae0ad666ea36ae031ae1cb08a7b5 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.30.7": version: 5.30.7 resolution: "@typescript-eslint/eslint-plugin@npm:5.30.7" @@ -10499,6 +11281,23 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.0.3": + version: 29.0.3 + resolution: "babel-jest@npm:29.0.3" + dependencies: + "@jest/transform": ^29.0.3 + "@types/babel__core": ^7.1.14 + babel-plugin-istanbul: ^6.1.1 + babel-preset-jest: ^29.0.2 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + slash: ^3.0.0 + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 4670945691c204464f7694017d59148b97cdbd51ff91ef492340ef5d6bbc74c461fa698a5feb04a93515300632ed44a55e85500bb61206d8a7ff60afb5b6da48 + languageName: node + linkType: hard + "babel-loader@npm:^8.0.0, babel-loader@npm:^8.1.0, babel-loader@npm:^8.2.5, babel-loader@npm:~8.2.3": version: 8.2.5 resolution: "babel-loader@npm:8.2.5" @@ -10601,6 +11400,18 @@ __metadata: languageName: node linkType: hard +"babel-plugin-jest-hoist@npm:^29.0.2": + version: 29.0.2 + resolution: "babel-plugin-jest-hoist@npm:29.0.2" + dependencies: + "@babel/template": ^7.3.3 + "@babel/types": ^7.3.3 + "@types/babel__core": ^7.1.14 + "@types/babel__traverse": ^7.0.6 + checksum: e02ab2c56b471940bc147d75808f6fb5d18b81382088beb36088d2fee8c5f9699b2a814a98884539191d43871d66770928e09c268c095ec39aad5766c3337f34 + languageName: node + linkType: hard + "babel-plugin-jsx-pragmatic@npm:^1.0.2": version: 1.0.2 resolution: "babel-plugin-jsx-pragmatic@npm:1.0.2" @@ -10652,6 +11463,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs2@npm:^0.3.3": + version: 0.3.3 + resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3" + dependencies: + "@babel/compat-data": ^7.17.7 + "@babel/helper-define-polyfill-provider": ^0.3.3 + semver: ^6.1.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7db3044993f3dddb3cc3d407bc82e640964a3bfe22de05d90e1f8f7a5cb71460011ab136d3c03c6c1ba428359ebf635688cd6205e28d0469bba221985f5c6179 + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs3@npm:^0.1.0": version: 0.1.7 resolution: "babel-plugin-polyfill-corejs3@npm:0.1.7" @@ -10676,6 +11500,18 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs3@npm:^0.6.0": + version: 0.6.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.6.0" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.3.3 + core-js-compat: ^3.25.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 470bb8c59f7c0912bd77fe1b5a2e72f349b3f65bbdee1d60d6eb7e1f4a085c6f24b2dd5ab4ac6c2df6444a96b070ef6790eccc9edb6a2668c60d33133bfb62c6 + languageName: node + linkType: hard + "babel-plugin-polyfill-regenerator@npm:^0.3.1": version: 0.3.1 resolution: "babel-plugin-polyfill-regenerator@npm:0.3.1" @@ -10687,6 +11523,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-regenerator@npm:^0.4.1": + version: 0.4.1 + resolution: "babel-plugin-polyfill-regenerator@npm:0.4.1" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ab0355efbad17d29492503230387679dfb780b63b25408990d2e4cf421012dae61d6199ddc309f4d2409ce4e9d3002d187702700dd8f4f8770ebbba651ed066c + languageName: node + linkType: hard + "babel-plugin-react-docgen@npm:^4.2.1": version: 4.2.1 resolution: "babel-plugin-react-docgen@npm:4.2.1" @@ -10750,6 +11597,18 @@ __metadata: languageName: node linkType: hard +"babel-preset-jest@npm:^29.0.2": + version: 29.0.2 + resolution: "babel-preset-jest@npm:29.0.2" + dependencies: + babel-plugin-jest-hoist: ^29.0.2 + babel-preset-current-node-syntax: ^1.0.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 485db525f4cd38c02c29edcd7240dd232e8d6dbcaef88bfa4765ad3057ed733512f1b7aad06f4bf9661afefeb0ada2c4e259d130113b0289d7db574f82bbd4f8 + languageName: node + linkType: hard + "babel-runtime@npm:^6.26.0": version: 6.26.0 resolution: "babel-runtime@npm:6.26.0" @@ -11374,6 +12233,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.21.3": + version: 4.21.3 + resolution: "browserslist@npm:4.21.3" + dependencies: + caniuse-lite: ^1.0.30001370 + electron-to-chromium: ^1.4.202 + node-releases: ^2.0.6 + update-browserslist-db: ^1.0.5 + bin: + browserslist: cli.js + checksum: ff512a7bcca1c530e2854bbdfc7be2791d0fb524097a6340e56e1d5924164c7e4e0a9b070de04cdc4c149d15cb4d4275cb7c626ebbce954278a2823aaad2452a + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -11873,6 +12746,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001370": + version: 1.0.30001399 + resolution: "caniuse-lite@npm:1.0.30001399" + checksum: dd105b06fbbdc89867780a2f4debc3ecb184cff82f35b34aaac486628fcc9cf6bacf37573a9cc22dedc661178d460fa8401374a933cb9d2f8ee67316d98b2a8f + languageName: node + linkType: hard + "capital-case@npm:^1.0.4": version: 1.0.4 resolution: "capital-case@npm:1.0.4" @@ -13053,6 +13933,15 @@ __metadata: languageName: node linkType: hard +"core-js-compat@npm:^3.25.1": + version: 3.25.1 + resolution: "core-js-compat@npm:3.25.1" + dependencies: + browserslist: ^4.21.3 + checksum: 34dbec657adc2f660f4cd701709c9c5e27cbd608211c65df09458f80f3e357b9492ba1c5173e17cca72d889dcc6da01268cadf88fb407cf1726e76d301c6143e + languageName: node + linkType: hard + "core-js-pure@npm:^3.8.1": version: 3.21.1 resolution: "core-js-pure@npm:3.21.1" @@ -14438,6 +15327,13 @@ __metadata: languageName: node linkType: hard +"diff-sequences@npm:^29.0.0": + version: 29.0.0 + resolution: "diff-sequences@npm:29.0.0" + checksum: 2c084a3db03ecde26f649f6f2559974e01e174451debeb301a7e17199e73423a8e8ddeb9a35ae38638c084b4fa51296a4a20fa7f44f3db0c0ba566bdc704ed3d + languageName: node + linkType: hard + "diff@npm:5.0.0": version: 5.0.0 resolution: "diff@npm:5.0.0" @@ -14876,6 +15772,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.202": + version: 1.4.249 + resolution: "electron-to-chromium@npm:1.4.249" + checksum: 830a35a157af7ae226f1528d727e369bb13f53bc7a4edefdf718651ace09d7d7b4bd7b70d33b5018b8eff6cf99ee58409b6c4140cd6d56350c1966f280ac5c93 + languageName: node + linkType: hard + "element-closest-polyfill@npm:^1.0.4": version: 1.0.5 resolution: "element-closest-polyfill@npm:1.0.5" @@ -14914,6 +15817,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.10.2": + version: 0.10.2 + resolution: "emittery@npm:0.10.2" + checksum: ee3e21788b043b90885b18ea756ec3105c1cedc50b29709c92b01e239c7e55345d4bb6d3aef4ddbaf528eef448a40b3bb831bad9ee0fc9c25cbf1367ab1ab5ac + languageName: node + linkType: hard + "emittery@npm:^0.8.1": version: 0.8.1 resolution: "emittery@npm:0.8.1" @@ -16063,6 +16973,19 @@ __metadata: languageName: node linkType: hard +"expect@npm:^29.0.3": + version: 29.0.3 + resolution: "expect@npm:29.0.3" + dependencies: + "@jest/expect-utils": ^29.0.3 + jest-get-type: ^29.0.0 + jest-matcher-utils: ^29.0.3 + jest-message-util: ^29.0.3 + jest-util: ^29.0.3 + checksum: 21b7fd346c47896a3de8f1103d7be32dab9409eb3dc170b7a9ff5d8d564b8499bd600b9af6251fe2f46064cf4e2f1456a6c6318da15314b7d74ed6dad723b555 + languageName: node + linkType: hard + "express-rate-limit@npm:^5.5.1": version: 5.5.1 resolution: "express-rate-limit@npm:5.5.1" @@ -16265,7 +17188,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0": +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb @@ -16286,6 +17209,13 @@ __metadata: languageName: node linkType: hard +"fast-redact@npm:^3.1.1": + version: 3.1.2 + resolution: "fast-redact@npm:3.1.2" + checksum: a30eb6b6830333ab213e0def55f46453ca777544dbd3a883016cb590a0eeb95e6fdf546553c1a13d509896bfba889b789991160a6d0996ceb19fce0a02e8b753 + languageName: node + linkType: hard + "fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -20529,6 +21459,16 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:^29.0.0": + version: 29.0.0 + resolution: "jest-changed-files@npm:29.0.0" + dependencies: + execa: ^5.0.0 + p-limit: ^3.1.0 + checksum: 5642ace8cd1e7e4f9e3ee423b97d0b018b00ad85ea7e5864592b4657e8500ef56ec50d2189229b912223046bbf31c9196c8ef2442a917be9726a5911d40db1b2 + languageName: node + linkType: hard + "jest-circus@npm:^27.5.1": version: 27.5.1 resolution: "jest-circus@npm:27.5.1" @@ -20556,6 +21496,33 @@ __metadata: languageName: node linkType: hard +"jest-circus@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-circus@npm:29.0.3" + dependencies: + "@jest/environment": ^29.0.3 + "@jest/expect": ^29.0.3 + "@jest/test-result": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + chalk: ^4.0.0 + co: ^4.6.0 + dedent: ^0.7.0 + is-generator-fn: ^2.0.0 + jest-each: ^29.0.3 + jest-matcher-utils: ^29.0.3 + jest-message-util: ^29.0.3 + jest-runtime: ^29.0.3 + jest-snapshot: ^29.0.3 + jest-util: ^29.0.3 + p-limit: ^3.1.0 + pretty-format: ^29.0.3 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 6ba495d4fb68ebb59f269b59029837f55009793d632ba2f29300992de80f7e3a37e619ea4e88676982cf74128416265a5929b3f9b77142fbf27c1dd0d6b6f98c + languageName: node + linkType: hard + "jest-cli@npm:^27.5.1": version: 27.5.1 resolution: "jest-cli@npm:27.5.1" @@ -20583,6 +21550,33 @@ __metadata: languageName: node linkType: hard +"jest-cli@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-cli@npm:29.0.3" + dependencies: + "@jest/core": ^29.0.3 + "@jest/test-result": ^29.0.3 + "@jest/types": ^29.0.3 + chalk: ^4.0.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + import-local: ^3.0.2 + jest-config: ^29.0.3 + jest-util: ^29.0.3 + jest-validate: ^29.0.3 + prompts: ^2.0.1 + yargs: ^17.3.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 4cd6ed7effcf703c3275bb07231227e32bd2de2dfc84354f0bbd25a2a8b26395570c8902c7dc87b02bed4a57c304a6b48e21a60fa26025b89c1e4e61eae1ea38 + languageName: node + linkType: hard + "jest-config@npm:^27.5.1": version: 27.5.1 resolution: "jest-config@npm:27.5.1" @@ -20620,6 +21614,44 @@ __metadata: languageName: node linkType: hard +"jest-config@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-config@npm:29.0.3" + dependencies: + "@babel/core": ^7.11.6 + "@jest/test-sequencer": ^29.0.3 + "@jest/types": ^29.0.3 + babel-jest: ^29.0.3 + chalk: ^4.0.0 + ci-info: ^3.2.0 + deepmerge: ^4.2.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-circus: ^29.0.3 + jest-environment-node: ^29.0.3 + jest-get-type: ^29.0.0 + jest-regex-util: ^29.0.0 + jest-resolve: ^29.0.3 + jest-runner: ^29.0.3 + jest-util: ^29.0.3 + jest-validate: ^29.0.3 + micromatch: ^4.0.4 + parse-json: ^5.2.0 + pretty-format: ^29.0.3 + slash: ^3.0.0 + strip-json-comments: ^3.1.1 + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: b2861ebf946e8c332c0526559de7f41d79bbe27731ee4de15add1a2ac8baec160ed572d22e66fd8dae6cde38dbedc9dd0987397021499f7aa44f558da651c65a + languageName: node + linkType: hard + "jest-diff@npm:^27.5.1": version: 27.5.1 resolution: "jest-diff@npm:27.5.1" @@ -20644,6 +21676,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-diff@npm:29.0.3" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.0.0 + jest-get-type: ^29.0.0 + pretty-format: ^29.0.3 + checksum: 1e12b63ea6254ea25146b6fb19f8b2d1ba334e1b8b635a007767c17dc272728afbdf41ccccce26c2a40cd9c7f3176bcfed53be2572927a3fc7b1ee5fff43eb26 + languageName: node + linkType: hard + "jest-docblock@npm:^27.5.1": version: 27.5.1 resolution: "jest-docblock@npm:27.5.1" @@ -20653,6 +21697,15 @@ __metadata: languageName: node linkType: hard +"jest-docblock@npm:^29.0.0": + version: 29.0.0 + resolution: "jest-docblock@npm:29.0.0" + dependencies: + detect-newline: ^3.0.0 + checksum: b4f81426cc0dffb05b873d3cc373a1643040be62d72cce4dfed499fbcb57c55ac02c44af7aba5e7753915ff5e85b8d6030456981156eaea20be1cb57d2719904 + languageName: node + linkType: hard + "jest-each@npm:^27.5.1": version: 27.5.1 resolution: "jest-each@npm:27.5.1" @@ -20666,6 +21719,19 @@ __metadata: languageName: node linkType: hard +"jest-each@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-each@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + chalk: ^4.0.0 + jest-get-type: ^29.0.0 + jest-util: ^29.0.3 + pretty-format: ^29.0.3 + checksum: 80c1912eb573a2972e29d9731cfbfa773b010c1416998eca28a90bda4f50de393c60860a2cb1531a4d3e0a0d23698c561a64e8942d48a75023b683136de519cc + languageName: node + linkType: hard + "jest-environment-jsdom@npm:^27.5.1": version: 27.5.1 resolution: "jest-environment-jsdom@npm:27.5.1" @@ -20695,6 +21761,20 @@ __metadata: languageName: node linkType: hard +"jest-environment-node@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-environment-node@npm:29.0.3" + dependencies: + "@jest/environment": ^29.0.3 + "@jest/fake-timers": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + jest-mock: ^29.0.3 + jest-util: ^29.0.3 + checksum: 76cd5759cdb08d3a4619004a23cc45fb8d103004b4d3e95451a36b981540c5d56e4f2a5b3cafb8ecf144bf874633ea86118a202e08aec1f445a25caf4081d8bc + languageName: node + linkType: hard + "jest-get-type@npm:^27.5.1": version: 27.5.1 resolution: "jest-get-type@npm:27.5.1" @@ -20709,6 +21789,13 @@ __metadata: languageName: node linkType: hard +"jest-get-type@npm:^29.0.0": + version: 29.0.0 + resolution: "jest-get-type@npm:29.0.0" + checksum: 9abdd11d69788963a92fb9d813a7b887654ecc8f3a3c8bf83166d33aaf4d57ed380e74ab8ef106f57565dd235446ca6ebc607679f0c516c4633e6d09f0540a2b + languageName: node + linkType: hard + "jest-haste-map@npm:^26.6.2": version: 26.6.2 resolution: "jest-haste-map@npm:26.6.2" @@ -20758,6 +21845,29 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-haste-map@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + "@types/graceful-fs": ^4.1.3 + "@types/node": "*" + anymatch: ^3.0.3 + fb-watchman: ^2.0.0 + fsevents: ^2.3.2 + graceful-fs: ^4.2.9 + jest-regex-util: ^29.0.0 + jest-util: ^29.0.3 + jest-worker: ^29.0.3 + micromatch: ^4.0.4 + walker: ^1.0.8 + dependenciesMeta: + fsevents: + optional: true + checksum: fb766e0d8174e7e3a43a63b28e23bd35db61a5939d6c5c1335d7f3d642d1c608e16fef8a105289b78795e308ab3176a62bc45acfa3fa14087e7635cb008795c3 + languageName: node + linkType: hard + "jest-jasmine2@npm:^27.5.1": version: 27.5.1 resolution: "jest-jasmine2@npm:27.5.1" @@ -20793,6 +21903,16 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-leak-detector@npm:29.0.3" + dependencies: + jest-get-type: ^29.0.0 + pretty-format: ^29.0.3 + checksum: a1657dbb72f2c3b4a8af148daec491d42eabdadc4e27eb8ec325d5267c21ac958e82e5c8ee679861c2131afd2bdfed4139b806511de7624d93b9838c6fcf5b2e + languageName: node + linkType: hard + "jest-matcher-utils@npm:^27.0.0, jest-matcher-utils@npm:^27.5.1": version: 27.5.1 resolution: "jest-matcher-utils@npm:27.5.1" @@ -20817,6 +21937,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-matcher-utils@npm:29.0.3" + dependencies: + chalk: ^4.0.0 + jest-diff: ^29.0.3 + jest-get-type: ^29.0.0 + pretty-format: ^29.0.3 + checksum: e39ab74a046ada8fbd75a275bfe54bd5f8ec14a98f77e1162a49d4e1ea82e68c5a4575691767cea0f60dd0b74cb481275012bf3467cd91fdb014311c670b8a83 + languageName: node + linkType: hard + "jest-message-util@npm:^27.5.1": version: 27.5.1 resolution: "jest-message-util@npm:27.5.1" @@ -20834,6 +21966,23 @@ __metadata: languageName: node linkType: hard +"jest-message-util@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-message-util@npm:29.0.3" + dependencies: + "@babel/code-frame": ^7.12.13 + "@jest/types": ^29.0.3 + "@types/stack-utils": ^2.0.0 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + micromatch: ^4.0.4 + pretty-format: ^29.0.3 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 04bee1fee10106f4eb875092e5d06187930d44050a4f99e7aa1d1e42768b18d6d9e5439623d9242202942deb8a1eec08359e0cd19a43ae505d96aeaf243a3f8d + languageName: node + linkType: hard + "jest-mock@npm:^27.0.6, jest-mock@npm:^27.5.1": version: 27.5.1 resolution: "jest-mock@npm:27.5.1" @@ -20844,6 +21993,16 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-mock@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + "@types/node": "*" + checksum: 8a04823334216f5fca9733a200cbb4cca207bdd74c523321a4170cbec3b2086b44eb1744a9faef808d2853593f132dda90d17e4bce59678fc373e1bab666ad0f + languageName: node + linkType: hard + "jest-pnp-resolver@npm:^1.2.2": version: 1.2.2 resolution: "jest-pnp-resolver@npm:1.2.2" @@ -20870,6 +22029,13 @@ __metadata: languageName: node linkType: hard +"jest-regex-util@npm:^29.0.0": + version: 29.0.0 + resolution: "jest-regex-util@npm:29.0.0" + checksum: dce16394c357213008e6f84f2288f77c64bba59b7cb48ea614e85c5aae036a7e46dbfd1f45aa08180b7e7c576102bf4f8f0ff8bc60fb9721fb80874adc3ae0ea + languageName: node + linkType: hard + "jest-resolve-dependencies@npm:^27.5.1": version: 27.5.1 resolution: "jest-resolve-dependencies@npm:27.5.1" @@ -20881,6 +22047,16 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-resolve-dependencies@npm:29.0.3" + dependencies: + jest-regex-util: ^29.0.0 + jest-snapshot: ^29.0.3 + checksum: 43980c0c03a7f00459209315832f03c28d8289ca30ccd8bb6652c87a2c03275aacdba8789177cefc162ceb05218ba3db8bf5a1968920aa4e510cbbefd54f9793 + languageName: node + linkType: hard + "jest-resolve@npm:^27.5.1": version: 27.5.1 resolution: "jest-resolve@npm:27.5.1" @@ -20899,6 +22075,23 @@ __metadata: languageName: node linkType: hard +"jest-resolve@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-resolve@npm:29.0.3" + dependencies: + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.0.3 + jest-pnp-resolver: ^1.2.2 + jest-util: ^29.0.3 + jest-validate: ^29.0.3 + resolve: ^1.20.0 + resolve.exports: ^1.1.0 + slash: ^3.0.0 + checksum: 9a774f78decbd9caa863e8c539d439aac76a780ea7acc54e90f2ad9c2000c03294e7f4f38816d16a8aa020ae0e3358845cc8f96fbab5f3e186b00e6e0462bf9b + languageName: node + linkType: hard + "jest-runner@npm:^27.5.1": version: 27.5.1 resolution: "jest-runner@npm:27.5.1" @@ -20928,6 +22121,35 @@ __metadata: languageName: node linkType: hard +"jest-runner@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-runner@npm:29.0.3" + dependencies: + "@jest/console": ^29.0.3 + "@jest/environment": ^29.0.3 + "@jest/test-result": ^29.0.3 + "@jest/transform": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + chalk: ^4.0.0 + emittery: ^0.10.2 + graceful-fs: ^4.2.9 + jest-docblock: ^29.0.0 + jest-environment-node: ^29.0.3 + jest-haste-map: ^29.0.3 + jest-leak-detector: ^29.0.3 + jest-message-util: ^29.0.3 + jest-resolve: ^29.0.3 + jest-runtime: ^29.0.3 + jest-util: ^29.0.3 + jest-watcher: ^29.0.3 + jest-worker: ^29.0.3 + p-limit: ^3.1.0 + source-map-support: 0.5.13 + checksum: db62830d1635be5e376fd261e38d37a4855146c9a586ec616cfb64257ef6f79697bd947bbb9751377dc2302626e73d1b77036eafd78ef6f93e1e53ca89c23e3e + languageName: node + linkType: hard + "jest-runtime@npm:^27.5.1": version: 27.5.1 resolution: "jest-runtime@npm:27.5.1" @@ -20958,6 +22180,36 @@ __metadata: languageName: node linkType: hard +"jest-runtime@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-runtime@npm:29.0.3" + dependencies: + "@jest/environment": ^29.0.3 + "@jest/fake-timers": ^29.0.3 + "@jest/globals": ^29.0.3 + "@jest/source-map": ^29.0.0 + "@jest/test-result": ^29.0.3 + "@jest/transform": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + chalk: ^4.0.0 + cjs-module-lexer: ^1.0.0 + collect-v8-coverage: ^1.0.0 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.0.3 + jest-message-util: ^29.0.3 + jest-mock: ^29.0.3 + jest-regex-util: ^29.0.0 + jest-resolve: ^29.0.3 + jest-snapshot: ^29.0.3 + jest-util: ^29.0.3 + slash: ^3.0.0 + strip-bom: ^4.0.0 + checksum: e13bfadfe225e9c06a95809d34e209e1769723c0c3d5913d86e4e748a22777e2bec11a352f2d12ca790e04203a6defc6556f77a3050518ebf6600a454a56fd36 + languageName: node + linkType: hard + "jest-serializer@npm:^26.6.2": version: 26.6.2 resolution: "jest-serializer@npm:26.6.2" @@ -21008,6 +22260,38 @@ __metadata: languageName: node linkType: hard +"jest-snapshot@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-snapshot@npm:29.0.3" + dependencies: + "@babel/core": ^7.11.6 + "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/traverse": ^7.7.2 + "@babel/types": ^7.3.3 + "@jest/expect-utils": ^29.0.3 + "@jest/transform": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/babel__traverse": ^7.0.6 + "@types/prettier": ^2.1.5 + babel-preset-current-node-syntax: ^1.0.0 + chalk: ^4.0.0 + expect: ^29.0.3 + graceful-fs: ^4.2.9 + jest-diff: ^29.0.3 + jest-get-type: ^29.0.0 + jest-haste-map: ^29.0.3 + jest-matcher-utils: ^29.0.3 + jest-message-util: ^29.0.3 + jest-util: ^29.0.3 + natural-compare: ^1.4.0 + pretty-format: ^29.0.3 + semver: ^7.3.5 + checksum: 412c0fc4c12c14470aa33beeddfeb04fa0d5724235321e8284b52097c74c97ada40ea52f5ac52a8e01e6d42dd5894b9a0260577d30c8c723ca84fcc7a60bd40c + languageName: node + linkType: hard + "jest-util@npm:^26.6.2": version: 26.6.2 resolution: "jest-util@npm:26.6.2" @@ -21036,6 +22320,20 @@ __metadata: languageName: node linkType: hard +"jest-util@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-util@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + "@types/node": "*" + chalk: ^4.0.0 + ci-info: ^3.2.0 + graceful-fs: ^4.2.9 + picomatch: ^2.2.3 + checksum: 39c31e75ba5bcb4c3ccdf0895f9fdbb83f839c432e7c6639a688beb414d681b5d50282da017c723ea1f2a7033e74a4938fd33dcff231c3e90f903173919991d5 + languageName: node + linkType: hard + "jest-validate@npm:^27.5.1": version: 27.5.1 resolution: "jest-validate@npm:27.5.1" @@ -21050,6 +22348,20 @@ __metadata: languageName: node linkType: hard +"jest-validate@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-validate@npm:29.0.3" + dependencies: + "@jest/types": ^29.0.3 + camelcase: ^6.2.0 + chalk: ^4.0.0 + jest-get-type: ^29.0.0 + leven: ^3.1.0 + pretty-format: ^29.0.3 + checksum: 096df6a77837155d9b65cd7ff9198489317c53903eb74a7f207e053c0b56204c18b6a8047e168eced291eb550b792ef4ab322b05c7da348af76cd78ea3556b4e + languageName: node + linkType: hard + "jest-watcher@npm:^27.5.1": version: 27.5.1 resolution: "jest-watcher@npm:27.5.1" @@ -21065,6 +22377,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-watcher@npm:29.0.3" + dependencies: + "@jest/test-result": ^29.0.3 + "@jest/types": ^29.0.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + emittery: ^0.10.2 + jest-util: ^29.0.3 + string-length: ^4.0.1 + checksum: d585b9dda467d08946357c8ed1f971f15a302f958ccd3f6e2e59df5da245edca91cd4a72329d0126de8ac5793567965e4be1e555c4e40ecadb4f8f14306441bb + languageName: node + linkType: hard + "jest-worker@npm:^26.5.0, jest-worker@npm:^26.6.2": version: 26.6.2 resolution: "jest-worker@npm:26.6.2" @@ -21087,6 +22415,17 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.0.3": + version: 29.0.3 + resolution: "jest-worker@npm:29.0.3" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: cdae4a58f6ab1ec3c384b42f1106004d434e65febcb34ba14a1e7d8538f7a5a5c2ebb0cf29cecfe8c71882c526ee02c4aa338a9ce0abcf11fcec9b8fa662189b + languageName: node + linkType: hard + "jest@npm:^27.5.1": version: 27.5.1 resolution: "jest@npm:27.5.1" @@ -21105,6 +22444,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.0.3": + version: 29.0.3 + resolution: "jest@npm:29.0.3" + dependencies: + "@jest/core": ^29.0.3 + "@jest/types": ^29.0.3 + import-local: ^3.0.2 + jest-cli: ^29.0.3 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 6bffa1ec703dbf64ce79aa7ef2887b586fdc96881cf2b83c8e86569237124a17aa001ddd4e7be7877202abe530ee5c04e9f0dd54e7320533b05b90709aca2607 + languageName: node + linkType: hard + "jmespath@npm:0.16.0": version: 0.16.0 resolution: "jmespath@npm:0.16.0" @@ -22635,6 +23993,15 @@ __metadata: languageName: node linkType: hard +"matchit@npm:^1.0.0": + version: 1.1.0 + resolution: "matchit@npm:1.1.0" + dependencies: + "@arr/every": ^1.0.0 + checksum: 14617343d9f77a5f2a2e9e5213b1d6c1c7b3efda10758d1502e632edf63bf77f260e3f1aa2afdc97dd7e259fae7845a1b07719cd4680d2f1d23b2aa45a393934 + languageName: node + linkType: hard + "mathml-tag-names@npm:^2.1.3": version: 2.1.3 resolution: "mathml-tag-names@npm:2.1.3" @@ -24251,6 +25618,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.6": + version: 2.0.6 + resolution: "node-releases@npm:2.0.6" + checksum: e86a926dc9fbb3b41b4c4a89d998afdf140e20a4e8dbe6c0a807f7b2948b42ea97d7fd3ad4868041487b6e9ee98409829c6e4d84a734a4215dff060a7fbeb4bf + languageName: node + linkType: hard + "node-rsa@npm:^1.1.1": version: 1.1.1 resolution: "node-rsa@npm:1.1.1" @@ -24769,6 +26143,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.0 + resolution: "on-exit-leak-free@npm:2.1.0" + checksum: 7334d98b87b0c89c9b69c747760b21196ff35afdedc4eaf1a0a3a02964463d7f6802481b120e4c8298967c74773ca7b914ab2eb3d9b279010eb7f67ac4960eed + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -25101,7 +26482,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.1, p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.1, p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -25855,6 +27236,16 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:v1.0.0": + version: 1.0.0 + resolution: "pino-abstract-transport@npm:1.0.0" + dependencies: + readable-stream: ^4.0.0 + split2: ^4.0.0 + checksum: 05dd0eda52dd99fd204b39fe7b62656744b63e863bc052cdd5105d25f226a236966d0a46e39a1ace4838f6e988c608837ff946d2d0bc92835ca7baa0a3bff8d8 + languageName: node + linkType: hard + "pino-pretty@npm:^7.6.1": version: 7.6.1 resolution: "pino-pretty@npm:7.6.1" @@ -25885,6 +27276,13 @@ __metadata: languageName: node linkType: hard +"pino-std-serializers@npm:^6.0.0": + version: 6.0.0 + resolution: "pino-std-serializers@npm:6.0.0" + checksum: d9dc1779b3870cdbe00dc2dff15e3931eb126bb144bc9f746d83a2c1174a28e366ed0abe63379dee2fee474e6018a088bfbb2c4b57c1e206601918f5a61e276f + languageName: node + linkType: hard + "pino@npm:^7.11.0": version: 7.11.0 resolution: "pino@npm:7.11.0" @@ -25906,6 +27304,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:^8.4.2": + version: 8.4.2 + resolution: "pino@npm:8.4.2" + dependencies: + atomic-sleep: ^1.0.0 + fast-redact: ^3.1.1 + on-exit-leak-free: ^2.1.0 + pino-abstract-transport: v1.0.0 + pino-std-serializers: ^6.0.0 + process-warning: ^2.0.0 + quick-format-unescaped: ^4.0.3 + real-require: ^0.2.0 + safe-stable-stringify: ^2.3.1 + sonic-boom: ^3.1.0 + thread-stream: ^2.0.0 + bin: + pino: bin.js + checksum: 870ee673d54a8e0b03766f39800e9805023f7f5efbbd33f61142febcdc556e24463d1d2af182c9b504e44ffa82bf692d67e4c284a64ea2cf96e166a13d77a0bc + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.5": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -26089,6 +27508,16 @@ __metadata: languageName: node linkType: hard +"polka@npm:^0.5.2": + version: 0.5.2 + resolution: "polka@npm:0.5.2" + dependencies: + "@polka/url": ^0.5.0 + trouter: ^2.0.1 + checksum: 5f4994e78985e10f77fcccced4d3781ba3f896059f66be6099ee189007b73a9bd4d136f73e4156f731bfe3ead66b1d44448b9be6e57e88fa28b53a6096bd3f11 + languageName: node + linkType: hard + "poly1305-js@npm:^0.4.2": version: 0.4.4 resolution: "poly1305-js@npm:0.4.4" @@ -27063,6 +28492,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^29.0.3": + version: 29.0.3 + resolution: "pretty-format@npm:29.0.3" + dependencies: + "@jest/schemas": ^29.0.0 + ansi-styles: ^5.0.0 + react-is: ^18.0.0 + checksum: 239aa73b09919b195353e62530908b43883af66e3ba8ecb5fda77578b20f297fd774fcf53abbedcb6cfff72521e8a220052a49e6a0e29418082d06386da27bac + languageName: node + linkType: hard + "pretty-hrtime@npm:^1.0.3": version: 1.0.3 resolution: "pretty-hrtime@npm:1.0.3" @@ -27100,6 +28540,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^2.0.0": + version: 2.0.0 + resolution: "process-warning@npm:2.0.0" + checksum: a2bb299835bced58e63cbe06a8fd6e048a648d3649e81b62c442b63112a3f0a86912e7b1a9c557daca30652232d3b0a7f1972fb87c36334e2a5a6f3d5c4a76c9 + languageName: node + linkType: hard + "process@npm:*, process@npm:^0.11.1, process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -28208,6 +29655,15 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.1.0 + resolution: "readable-stream@npm:4.1.0" + dependencies: + abort-controller: ^3.0.0 + checksum: ff2bb513af6fb43618c8360211b5b9052e25a59e6626d3669c7ba060d021dfffa43c43832e11b18acd6aac15b057c6deae1c41004c1731688c95c455ad02f982 + languageName: node + linkType: hard + "readable-stream@npm:~2.0.5": version: 2.0.6 resolution: "readable-stream@npm:2.0.6" @@ -28258,6 +29714,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: fa060f19f2f447adf678d1376928c76379dce5f72bd334da301685ca6cdcb7b11356813332cc243c88470796bc2e2b1e2917fc10df9143dd93c2ea608694971d + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -29014,12 +30477,13 @@ __metadata: moleculer: ^0.14.21 mongodb: ^4.3.1 nats: ^2.6.1 + npm-run-all: ^4.1.5 pino: ^7.11.0 pino-pretty: ^7.6.1 pm2: ^5.2.0 sodium-native: ^3.3.0 sodium-plus: ^0.9.0 - ts-node: ^10.8.1 + ts-node: ^10.9.1 typescript: ~4.5.5 underscore.string: ^3.3.6 uuid: ^8.3.2 @@ -30001,6 +31465,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^3.1.0": + version: 3.2.0 + resolution: "sonic-boom@npm:3.2.0" + dependencies: + atomic-sleep: ^1.0.0 + checksum: 526669b78e0ac3bcbe2a53e5ac8960d3b25e61d8e6a46eaed5a0c46d7212c5f638bb136236870babedfcb626063711ba8f81e538f88b79e6a90a5b2ff71943b4 + languageName: node + linkType: hard + "sort-keys-length@npm:^1.0.0": version: 1.0.1 resolution: "sort-keys-length@npm:1.0.1" @@ -30055,6 +31528,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 933550047b6c1a2328599a21d8b7666507427c0f5ef5eaadd56b5da0fd9505e239053c66fe181bf1df469a3b7af9d775778eee283cbb7ae16b902ddc09e93a97 + languageName: node + linkType: hard + "source-map-support@npm:0.5.19": version: 0.5.19 resolution: "source-map-support@npm:0.5.19" @@ -31576,6 +33059,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^2.0.0": + version: 2.1.0 + resolution: "thread-stream@npm:2.1.0" + dependencies: + real-require: ^0.2.0 + checksum: f74b6ebaaeaff1ddc1c683fd27fddbf719dc5ba19b8face9019039295cacc3317458d65c9ac7ab7301413667ef15f67ca851adc37cb9273436eef3155541c4c2 + languageName: node + linkType: hard + "thriftrw@npm:^3.5.0": version: 3.12.0 resolution: "thriftrw@npm:3.12.0" @@ -31929,6 +33421,15 @@ __metadata: languageName: node linkType: hard +"trouter@npm:^2.0.1": + version: 2.0.1 + resolution: "trouter@npm:2.0.1" + dependencies: + matchit: ^1.0.0 + checksum: 4a25e81a132d75e8659a29c4b1f6a91eff06601a78a9d1fc189d525ad0298881ed7db7a82045a3b7d1fcc52cb283d2ca7b79eb908de02088798de36659d5205c + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -31985,9 +33486,9 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.8.1": - version: 10.8.1 - resolution: "ts-node@npm:10.8.1" +"ts-node@npm:^10.9.1": + version: 10.9.1 + resolution: "ts-node@npm:10.9.1" dependencies: "@cspotcode/source-map-support": ^0.8.0 "@tsconfig/node10": ^1.0.7 @@ -32019,7 +33520,7 @@ __metadata: ts-node-script: dist/bin-script.js ts-node-transpile-only: dist/bin-transpile.js ts-script: dist/bin-script-deprecated.js - checksum: 7d1aa7aa3ae1c0459c4922ed0dbfbade442cfe0c25aebaf620cdf1774f112c8d7a9b14934cb6719274917f35b2c503ba87bcaf5e16a0d39ba0f68ce3e7728363 + checksum: 090adff1302ab20bd3486e6b4799e90f97726ed39e02b39e566f8ab674fd5bd5f727f43615debbfc580d33c6d9d1c6b1b3ce7d8e3cca3e20530a145ffa232c35 languageName: node linkType: hard @@ -32826,6 +34327,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.0.5": + version: 1.0.9 + resolution: "update-browserslist-db@npm:1.0.9" + dependencies: + escalade: ^3.1.1 + picocolors: ^1.0.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + browserslist-lint: cli.js + checksum: f625899b236f6a4d7f62b56be1b8da230c5563d1fef84d3ef148f2e1a3f11a5a4b3be4fd7e3703e51274c116194017775b10afb4de09eb2c0d09d36b90f1f578 + languageName: node + linkType: hard + "update-check@npm:1.5.2": version: 1.5.2 resolution: "update-check@npm:1.5.2" @@ -33191,6 +34706,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.0.1 + resolution: "v8-to-istanbul@npm:9.0.1" + dependencies: + "@jridgewell/trace-mapping": ^0.3.12 + "@types/istanbul-lib-coverage": ^2.0.1 + convert-source-map: ^1.6.0 + checksum: a49c34bf0a3af0c11041a3952a2600913904a983bd1bc87148b5c033bc5c1d02d5a13620fcdbfa2c60bc582a2e2970185780f0c844b4c3a220abf405f8af6311 + languageName: node + linkType: hard + "valid-data-url@npm:^3.0.0": version: 3.0.1 resolution: "valid-data-url@npm:3.0.1" @@ -33350,7 +34876,7 @@ __metadata: languageName: node linkType: hard -"walker@npm:^1.0.7, walker@npm:~1.0.5": +"walker@npm:^1.0.7, walker@npm:^1.0.8, walker@npm:~1.0.5": version: 1.0.8 resolution: "walker@npm:1.0.8" dependencies: @@ -34474,6 +36000,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.0.0": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" @@ -34538,6 +36071,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.3.1": + version: 17.5.1 + resolution: "yargs@npm:17.5.1" + dependencies: + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.0.0 + checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde + languageName: node + linkType: hard + "yargs@npm:^2.1.1": version: 2.3.0 resolution: "yargs@npm:2.3.0" -- GitLab From 34c07d2dc18812e7b4a8697d563df13af120fd6d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Thu, 15 Sep 2022 20:17:14 -0300 Subject: [PATCH 042/107] Chore: Move Omnichannel Room Footer to react (#26864) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Co-authored-by: Martin Schoeler <20868078+MartinSchoeler@users.noreply.github.com> --- .../views/app/livechatNotSubscribed.html | 3 - .../client/views/app/livechatReadOnly.html | 28 - .../client/views/app/livechatReadOnly.js | 129 -- .../app/livechat/client/views/regular.js | 2 - apps/meteor/app/models/client/models/Users.js | 2 +- .../client/messageBox/messageBox.html | 150 +- .../client/messageBox/messageBox.ts | 2 - .../messageBox/messageBoxNotSubscribed.html | 26 - .../messageBox/messageBoxNotSubscribed.ts | 72 - .../client/messageBox/messageBoxReadOnly.html | 14 - .../client/messageBox/messageBoxReadOnly.ts | 10 - .../views/room/components/body/RoomBody.tsx | 2 +- .../body/composer/ComposerAnonymous.tsx | 27 + .../body/composer/ComposerBlocked.tsx | 8 + .../body/composer/ComposerContainer.tsx | 67 + .../composer/ComposerJoinWithPassword.tsx | 54 + .../ComposerMessage.tsx} | 20 +- .../ComposerOmnichannel.tsx | 57 + .../ComposerOmnichannelInquiry.tsx | 42 + .../ComposerOmnichannelJoin.tsx | 34 + .../ComposerOmnichannelOnHold.tsx | 19 + .../composer/ComposerOmnichannel/index.tsx | 1 + .../body/composer/ComposerReadOnly.tsx | 8 + .../ComposerUsersAction.tsx | 3 + .../body/composer/MessageComposer.spec.ts | 14 + .../hooks/useMessageComposerCanJoin.ts | 8 + .../hooks/useMessageComposerIsAnonymous.ts | 13 + .../hooks/useMessageComposerIsBlocked.ts | 18 + .../hooks/useMessageComposerIsReadOnly.ts | 20 + .../client/views/room/contexts/RoomContext.ts | 10 + apps/meteor/definition/IRoomTypeConfig.ts | 6 +- apps/meteor/package.json | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 5 + .../page-objects/fragments/home-content.ts | 2 +- packages/core-typings/src/IRoom.ts | 4 +- packages/rest-typings/src/v1/omnichannel.ts | 6 + packages/ui-composer/.eslintrc.json | 4 + packages/ui-composer/.storybook/main.js | 12 + packages/ui-composer/.storybook/preview.js | 25 + packages/ui-composer/package.json | 46 + .../MessageComposer.stories.tsx | 69 + .../src/MessageComposer/MessageComposer.tsx | 26 + .../MessageComposer/MessageComposerAction.tsx | 6 + .../MessageComposerActionsDivider.tsx | 8 + .../MessageComposer/MessageComposerIcon.tsx | 9 + .../MessageComposer/MessageComposerInput.tsx | 6 + .../MessageComposerSkeleton.tsx | 31 + .../MessageComposerToolbar.tsx | 8 + .../MessageComposerToolbarActions.tsx | 6 + .../MessageComposerToolbarSubmit.tsx | 6 + .../ui-composer/src/MessageComposer/index.ts | 21 + .../MessageFooterCallout.stories.tsx | 53 + .../MessageFooterCallout.tsx | 34 + .../MessageFooterCalloutAction.tsx | 9 + .../MessageFooterCalloutContent.tsx | 12 + .../MessageFooterCalloutDivider.tsx | 9 + .../src/MessageFooterCallout/index.ts | 6 + packages/ui-composer/src/index.ts | 2 + packages/ui-composer/tsconfig.json | 8 + .../ui-contexts/src/ServerContext/methods.ts | 2 + yarn.lock | 1319 ++++++++++++++++- 61 files changed, 2233 insertions(+), 391 deletions(-) delete mode 100644 apps/meteor/app/livechat/client/views/app/livechatNotSubscribed.html delete mode 100644 apps/meteor/app/livechat/client/views/app/livechatReadOnly.html delete mode 100644 apps/meteor/app/livechat/client/views/app/livechatReadOnly.js delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.html delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.ts delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.html delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerAnonymous.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerBlocked.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerJoinWithPassword.tsx rename apps/meteor/client/views/room/components/body/{ComposerContainer.tsx => composer/ComposerMessage.tsx} (77%) create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelJoin.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/index.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerReadOnly.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerUsersAction/ComposerUsersAction.tsx create mode 100644 apps/meteor/client/views/room/components/body/composer/MessageComposer.spec.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerCanJoin.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsAnonymous.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsBlocked.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsReadOnly.ts create mode 100644 packages/ui-composer/.eslintrc.json create mode 100644 packages/ui-composer/.storybook/main.js create mode 100644 packages/ui-composer/.storybook/preview.js create mode 100644 packages/ui-composer/package.json create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposer.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerAction.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerActionsDivider.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerIcon.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerSkeleton.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx create mode 100644 packages/ui-composer/src/MessageComposer/MessageComposerToolbarSubmit.tsx create mode 100644 packages/ui-composer/src/MessageComposer/index.ts create mode 100644 packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.stories.tsx create mode 100644 packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.tsx create mode 100644 packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutAction.tsx create mode 100644 packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx create mode 100644 packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutDivider.tsx create mode 100644 packages/ui-composer/src/MessageFooterCallout/index.ts create mode 100644 packages/ui-composer/src/index.ts create mode 100644 packages/ui-composer/tsconfig.json diff --git a/apps/meteor/app/livechat/client/views/app/livechatNotSubscribed.html b/apps/meteor/app/livechat/client/views/app/livechatNotSubscribed.html deleted file mode 100644 index 97e7c7e9f30..00000000000 --- a/apps/meteor/app/livechat/client/views/app/livechatNotSubscribed.html +++ /dev/null @@ -1,3 +0,0 @@ -<template name="livechatNotSubscribed"> - <p>{{_ "This_conversation_is_already_closed"}}</p> -</template> diff --git a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.html b/apps/meteor/app/livechat/client/views/app/livechatReadOnly.html deleted file mode 100644 index 0ff465acd25..00000000000 --- a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.html +++ /dev/null @@ -1,28 +0,0 @@ -<template name="livechatReadOnly"> - {{#if isPreparing}} - {{> loading}} - {{else}} - {{#if roomOpen}} - {{#if isOnHold}} - <div class="rc-message-box__join"> - {{{_ "chat_on_hold_due_to_inactivity"}}} - <button class="rc-button rc-button--primary rc-button--small rc-message-box__resume-it-button js-resume-it">{{_ "Resume"}}</button> - </div> - {{else}} - {{#if inquiryOpen}} - <div class="rc-message-box__join"> - {{{_ "you_are_in_preview_mode_of_incoming_livechat"}}} - <button class="rc-button rc-button--primary rc-button--small rc-message-box__take-it-button js-take-it">{{_ "Take_it"}}</button> - </div> - {{else}} - <div class="rc-message-box__join"> - {{{_ "room_is_read_only"}}} - <button class="rc-button rc-button--primary rc-button--small rc-message-box__join-it-button js-join-it">{{_ "Join"}}</button> - </div> - {{/if}} - {{/if}} - {{else}} - <p>{{_ "This_conversation_is_already_closed"}}</p> - {{/if}} - {{/if}} -</template> diff --git a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js b/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js deleted file mode 100644 index 8df23b05e20..00000000000 --- a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js +++ /dev/null @@ -1,129 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { ChatRoom, CachedChatRoom } from '../../../../models/client'; -import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; -import { APIClient } from '../../../../utils/client'; -import { inquiryDataStream } from '../../lib/stream/inquiry'; -import { dispatchToastMessage } from '../../../../../client/lib/toast'; -import './livechatReadOnly.html'; - -Template.livechatReadOnly.helpers({ - inquiryOpen() { - const inquiry = Template.instance().inquiry.get(); - return inquiry && inquiry.status === 'queued'; - }, - - roomOpen() { - const room = Template.instance().room.get(); - return room && room.open === true; - }, - - showPreview() { - const config = Template.instance().routingConfig.get(); - return config.previewRoom || Template.currentData().onHold; - }, - - isPreparing() { - return Template.instance().preparing.get(); - }, - - isOnHold() { - return Template.currentData().onHold; - }, -}); - -Template.livechatReadOnly.events({ - async 'click .js-take-it'(event, instance) { - event.preventDefault(); - event.stopPropagation(); - - const inquiry = instance.inquiry.get(); - const { _id } = inquiry; - await callWithErrorHandling('livechat:takeInquiry', _id, { clientAction: true }); - instance.loadInquiry(inquiry.rid); - }, - - async 'click .js-resume-it'(event, instance) { - event.preventDefault(); - event.stopPropagation(); - - const room = instance.room.get(); - - await callWithErrorHandling('livechat:resumeOnHold', room._id, { clientAction: true }); - }, - - async 'click .js-join-it'(event) { - event.preventDefault(); - event.stopPropagation(); - - try { - const { success } = (await APIClient.get(`/v1/livechat/room.join`, { roomId: this.rid })) || {}; - if (!success) { - throw new Meteor.Error('error-join-room', 'Error joining room'); - } - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - throw error; - } - }, -}); - -Template.livechatReadOnly.onCreated(async function () { - this.rid = Template.currentData().rid; - this.room = new ReactiveVar(); - this.inquiry = new ReactiveVar(); - this.routingConfig = new ReactiveVar({}); - this.preparing = new ReactiveVar(true); - this.updateInquiry = async ({ clientAction, ...inquiry }) => { - if (clientAction === 'removed') { - // this will force to refresh the room - // since the client wont get notified of room changes when chats are on queue (no one assigned) - // a better approach should be performed when refactoring these templates to use react - ChatRoom.remove(this.rid); - CachedChatRoom.save(); - return FlowRouter.go('/home'); - } - - this.inquiry.set(inquiry); - }; - - this.roomDataStream = new Meteor.Streamer('room-data'); - - Meteor.call('livechat:getRoutingConfig', (err, config) => { - if (config) { - this.routingConfig.set(config); - } - }); - - this.loadRoomAndInquiry = async (roomId) => { - this.preparing.set(true); - const { inquiry } = await APIClient.get(`/v1/livechat/inquiries.getOne`, { roomId }); - this.inquiry.set(inquiry); - if (inquiry && inquiry._id) { - inquiryDataStream.on(inquiry._id, this.updateInquiry); - } - - const { room } = await APIClient.get(`/v1/rooms.info`, { roomId }); - this.room.set(room); - if (room && room._id) { - this.roomDataStream.on(roomId, (room) => this.room.set(room)); - } - - this.preparing.set(false); - }; - - this.autorun(() => this.loadRoomAndInquiry(this.rid)); -}); - -Template.livechatReadOnly.onDestroyed(function () { - const inquiry = this.inquiry.get(); - if (inquiry && inquiry._id) { - inquiryDataStream.removeListener(inquiry._id, this.updateInquiry); - } - - const { rid } = Template.currentData(); - this.roomDataStream.removeListener(rid); -}); diff --git a/apps/meteor/app/livechat/client/views/regular.js b/apps/meteor/app/livechat/client/views/regular.js index e81ecf8ca51..8bb4b9f4845 100644 --- a/apps/meteor/app/livechat/client/views/regular.js +++ b/apps/meteor/app/livechat/client/views/regular.js @@ -1,5 +1,3 @@ -import './app/livechatReadOnly'; -import './app/livechatNotSubscribed.html'; import './app/tabbar/contactChatHistory'; import './app/tabbar/contactChatHistoryItem'; import './app/tabbar/contactChatHistoryMessages'; diff --git a/apps/meteor/app/models/client/models/Users.js b/apps/meteor/app/models/client/models/Users.js index 2a5e46bec2f..c853d21407c 100644 --- a/apps/meteor/app/models/client/models/Users.js +++ b/apps/meteor/app/models/client/models/Users.js @@ -24,7 +24,7 @@ export const Users = { * @param {string} scope the value for the scope (room id) * @param {any} options */ - findUsersInRoles(roles, scope, options) { + findUsersInRoles(roles, _scope, options) { roles = [].concat(roles); const query = { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index ad15ba0d9b3..c1e944faaed 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -1,100 +1,86 @@ <template name="messageBox"> <div class="rc-message-box rc-new {{#if isEmbedded}}rc-message-box--embedded{{/if}}"> - {{#unless isAnonymousOrMustJoinWithCode}} - {{> userActionIndicator rid=rid tmid=tmid}} + {{> userActionIndicator rid=rid tmid=tmid}} - {{#if isWritable}} - {{#if popupConfig}} - {{> messagePopupConfig popupConfig}} - {{> messagePopupSlashCommandPreview popupConfig}} - {{/if}} + {{#if popupConfig}} + {{> messagePopupConfig popupConfig}} + {{> messagePopupSlashCommandPreview popupConfig}} + {{/if}} - {{#if replyMessageData}} - <div class="reply-preview__wrap message-popup"> - {{#each reply in replyMessageData}} - {{> messageBoxReplyPreview input=input replyMessageData=reply}} - {{/each}} - </div> - {{/if}} + {{#if replyMessageData}} + <div class="reply-preview__wrap message-popup"> + {{#each reply in replyMessageData}} + {{> messageBoxReplyPreview input=input replyMessageData=reply}} + {{/each}} + </div> + {{/if}} - <label class="rc-message-box__container"> + <label class="rc-message-box__container"> + {{#unless isFederatedRoom}} + <span class="rc-message-box__icon emoji-picker-icon {{#unless isEmojiEnabled}}emoji-picker-icon--disabled{{/unless}} js-emoji-picker" aria-haspopup="true"> + {{> icon block="rc-input__icon-svg" icon="emoji"}} + </span> + {{else}} + <div class="rc-message-box__federation_icon"></div> + {{/unless}} + + <textarea aria-label="{{_ 'Message'}}" name="msg" maxlength="{{maxMessageLength}}" placeholder="{{_ 'Message'}}" rows="1" class="rc-message-box__textarea js-input-message"></textarea> + <div class="js-input-message-shadow"></div> + {{#if isSendIconVisible}} + <span class="rc-message-box__icon rc-message-box__send js-send" data-desktop> + {{> icon block="rc-input__icon-svg" icon="send"}} + </span> + {{else}} + {{# if customAction }} + {{> Template.dynamic template=customAction.template data=customAction.data }} + {{ else }} {{#unless isFederatedRoom}} - <span class="rc-message-box__icon emoji-picker-icon {{#unless isEmojiEnabled}}emoji-picker-icon--disabled{{/unless}} js-emoji-picker" aria-haspopup="true"> - {{> icon block="rc-input__icon-svg" icon="emoji"}} - </span> - {{else}} - <div class="rc-message-box__federation_icon"></div> - {{/unless}} - - <textarea aria-label="{{_ 'Message'}}" name="msg" maxlength="{{maxMessageLength}}" placeholder="{{_ 'Message'}}" rows="1" class="rc-message-box__textarea js-input-message"></textarea> - <div class="js-input-message-shadow"></div> - {{#if isSendIconVisible}} - <span class="rc-message-box__icon rc-message-box__send js-send" data-desktop> - {{> icon block="rc-input__icon-svg" icon="send"}} - </span> - {{else}} - {{# if customAction }} - {{> Template.dynamic template=customAction.template data=customAction.data }} - {{ else }} - {{#unless isFederatedRoom}} - {{#if canSend}} - {{> messageBoxAudioMessage rid=rid tmid=tmid}} - <span class="rc-message-box__action-menu js-action-menu" data-desktop aria-haspopup="true" data-qa-id="menu-more-actions"> - {{#if actions}} - <span class="rc-message-box__icon"> - {{> icon block="rc-input__icon-svg" icon="plus"}} - </span> - {{/if}} + {{#if canSend}} + {{> messageBoxAudioMessage rid=rid tmid=tmid}} + <span class="rc-message-box__action-menu js-action-menu" data-desktop aria-haspopup="true" data-qa-id="menu-more-actions"> + {{#if actions}} + <span class="rc-message-box__icon"> + {{> icon block="rc-input__icon-svg" icon="plus"}} </span> - {{else}} - <button class="js-join rc-button rc-button--primary rc-message-box__join-button"> - {{_ "join"}} - </button> {{/if}} - {{/unless}} + </span> + {{else}} + <button class="js-join rc-button rc-button--primary rc-message-box__join-button"> + {{_ "join"}} + </button> {{/if}} - {{/if}} + {{/unless}} + {{/if}} + {{/if}} - <span class="rc-message-box__action-label js-message-actions" data-small> - {{#each action in actions}} - <span class="js-message-action" data-id="{{action.id}}"> - {{> icon block="rc-message-box__action" icon=action.icon }} - </span> - {{/each}} + <span class="rc-message-box__action-label js-message-actions" data-small> + {{#each action in actions}} + <span class="js-message-action" data-id="{{action.id}}"> + {{> icon block="rc-message-box__action" icon=action.icon }} </span> + {{/each}} + </span> - <span class="rc-message-box__send js-message-action js-send" disabled="{{isSendIconDisabled}}" data-small> - {{> icon block="rc-message-box__action" icon="send"}} - </span> - </label> + <span class="rc-message-box__send js-message-action js-send" disabled="{{isSendIconDisabled}}" data-small> + {{> icon block="rc-message-box__action" icon="send"}} + </span> + </label> - {{#if showFormattingTips}} - <div class="rc-message-box__toolbar-formatting"> - {{#each formattingButton in formattingButtons}} - {{#if formattingButton.icon}} - <button class="rc-message-box__toolbar-formatting-item js-format" data-id="{{formattingButton.label}}" title="{{_ formattingButton.label}}"> - {{> icon block="rc-message-box__toolbar-formatting-icon" icon=formattingButton.icon }} - </button> - {{else}} - <span class="rc-message-box__toolbar-formatting-item" title="{{_ formattingButton.label}}"> - <a href="{{formattingButton.link}}" target="_blank" rel="noopener noreferrer" class="rc-message-box__toolbar-formatting-link">{{formattingButton.text}}</a> - </span> - {{/if}} - {{/each}} - </div> - {{/if}} - {{else}} - <div class="rc-message-box__cannot-send"> - {{#if isBlockedOrBlocker}} - {{_ "room_is_blocked"}} + {{#if showFormattingTips}} + <div class="rc-message-box__toolbar-formatting"> + {{#each formattingButton in formattingButtons}} + {{#if formattingButton.icon}} + <button class="rc-message-box__toolbar-formatting-item js-format" data-id="{{formattingButton.label}}" title="{{_ formattingButton.label}}"> + {{> icon block="rc-message-box__toolbar-formatting-icon" icon=formattingButton.icon }} + </button> {{else}} - {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed onHold=onHold }} + <span class="rc-message-box__toolbar-formatting-item" title="{{_ formattingButton.label}}"> + <a href="{{formattingButton.link}}" target="_blank" rel="noopener noreferrer" class="rc-message-box__toolbar-formatting-link">{{formattingButton.text}}</a> + </span> {{/if}} - </div> - {{/if}} - {{else}} - {{> messageBoxNotSubscribed rid=rid}} - {{/unless}} + {{/each}} + </div> + {{/if}} </div> </template> diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index df0a8a1d52e..33e44e77b5a 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -26,8 +26,6 @@ import './messageBoxActions'; import './messageBoxReplyPreview.ts'; import './userActionIndicator.ts'; import './messageBoxAudioMessage.ts'; -import './messageBoxNotSubscribed.ts'; -import './messageBoxReadOnly.ts'; import './messageBox.html'; type MessageBoxTemplateInstance = Blaze.TemplateInstance<{ diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.html deleted file mode 100644 index 78e764a693d..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.html +++ /dev/null @@ -1,26 +0,0 @@ -<template name="messageBoxNotSubscribed" args="rid"> - {{#if customTemplate}} - {{> Template.dynamic template=customTemplate }} - {{else}} - {{#if canJoinRoom}} - <div class="rc-message-box__join"> - {{{_ "you_are_in_preview_mode_of" room_name=roomName}}} - - {{#if isJoinCodeRequired}} - <input type="password" name="joinCode" placeholder="{{_ 'Password'}}" class="rc-input__element rc-message-box__join-code"> - {{/if}} - - <button class="rc-button rc-button--primary rc-button--small rc-message-box__join-button js-join-code">{{_ "join"}}</button> - </div> - {{/if}} - - {{#if isAnonymousReadAllowed}} - <div class="rc-button-group"> - <button class="rc-button rc-button--primary rc-button--small js-register">{{_ "Sign_in_to_start_talking"}}</button> - {{#if isAnonymousWriteAllowed}} - <button class="rc-button rc-button--small js-register-anonymous">{{_ "Or_talk_as_anonymous"}}</button> - {{/if}} - </div> - {{/if}} - {{/if}} -</template> diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.ts deleted file mode 100644 index c34094fd1df..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxNotSubscribed.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../settings/client'; -import { RoomManager, RoomHistoryManager } from '../../../ui-utils/client'; -import { hasAllPermission } from '../../../authorization/client'; -import { call } from '../../../../client/lib/utils/call'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import './messageBoxNotSubscribed.html'; - -Template.messageBoxNotSubscribed.helpers({ - customTemplate() { - return roomCoordinator.getRoomTypeConfigById(this.rid)?.notSubscribedTpl; - }, - canJoinRoom() { - return Meteor.userId() && roomCoordinator.getRoomDirectivesById(this.rid)?.showJoinLink(this.rid); - }, - roomName() { - const room = Session.get(`roomData${this.rid}`); - return roomCoordinator.getRoomName(room.t, room); - }, - isJoinCodeRequired() { - const room = Session.get(`roomData${this.rid}`); - return room?.joinCodeRequired; - }, - isAnonymousReadAllowed() { - return Meteor.userId() == null && settings.get('Accounts_AllowAnonymousRead') === true; - }, - isAnonymousWriteAllowed() { - return ( - Meteor.userId() == null && - settings.get('Accounts_AllowAnonymousRead') === true && - settings.get('Accounts_AllowAnonymousWrite') === true - ); - }, -}); - -Template.messageBoxNotSubscribed.events({ - async 'click .js-join-code'(event: JQuery.ClickEvent) { - event.stopPropagation(); - event.preventDefault(); - - const joinCodeInput = Template.instance().find('[name=joinCode]') as HTMLInputElement; - const joinCode = joinCodeInput?.value; - - await call('joinRoom', this.rid, joinCode); - - if (hasAllPermission('preview-c-room') === false && RoomHistoryManager.getRoom(this.rid).loaded === 0) { - const openedRoom = RoomManager.getOpenedRoomByRid(this.rid); - if (openedRoom) { - openedRoom.streamActive = false; - openedRoom.ready = false; - } - RoomHistoryManager.getRoom(this.rid).loaded = undefined; - RoomManager.computation.invalidate(); - } - }, - 'click .js-register'(event: JQuery.ClickEvent) { - event.stopPropagation(); - event.preventDefault(); - - Session.set('forceLogin', true); - }, - async 'click .js-register-anonymous'(event: JQuery.ClickEvent) { - event.stopPropagation(); - event.preventDefault(); - - const { token } = await call('registerUser', {}); - Meteor.loginWithToken(token); - }, -}); diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.html deleted file mode 100644 index c6608660d78..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.html +++ /dev/null @@ -1,14 +0,0 @@ -<template name="messageBoxReadOnly" args="rid, isSubscribed"> - {{#if customTemplate}} - {{> Template.dynamic template=customTemplate }} - {{else}} - <span> - {{_ "room_is_read_only"}} - </span> - {{#unless isSubscribed}} - <button class="js-join rc-button rc-button--primary rc-message-box__join-button"> - {{_ "join"}} - </button> - {{/unless}} - {{/if}} -</template> diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.ts deleted file mode 100644 index 36a91b9eebe..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxReadOnly.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Template } from 'meteor/templating'; - -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import './messageBoxReadOnly.html'; - -Template.messageBoxReadOnly.helpers({ - customTemplate() { - return roomCoordinator.getRoomTypeConfigById(this.rid)?.readOnlyTpl ?? false; - }, -}); diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 4050ff1ffc4..0f9beecf4c0 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -30,7 +30,6 @@ import Announcement from '../../Announcement'; import { MessageList } from '../../MessageList/MessageList'; import { useRoom } from '../../contexts/RoomContext'; import { useTabBarAPI } from '../../providers/ToolboxProvider'; -import ComposerContainer from './ComposerContainer'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessagesBar from './JumpToRecentMessagesBar'; import LeaderBar from './LeaderBar'; @@ -41,6 +40,7 @@ import RetentionPolicyWarning from './RetentionPolicyWarning'; import RoomForeword from './RoomForeword'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import UploadProgressIndicator from './UploadProgressIndicator'; +import ComposerContainer from './composer/ComposerContainer'; import { useChatMessages } from './useChatMessages'; import { useFileUploadDropTarget } from './useFileUploadDropTarget'; import { useRetentionPolicy } from './useRetentionPolicy'; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerAnonymous.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerAnonymous.tsx new file mode 100644 index 00000000000..0ba7ddabfab --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerAnonymous.tsx @@ -0,0 +1,27 @@ +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +export const ComposerAnonymous = (): ReactElement => { + const isAnonymousWriteEnabled = useSetting('Accounts_AllowAnonymousWrite'); + // 'click .js-register'(event: JQuery.ClickEvent) { + // event.stopPropagation(); + // event.preventDefault(); + + // Session.set('forceLogin', true); + // }, + // async 'click .js-register-anonymous'(event: JQuery.ClickEvent) { + // event.stopPropagation(); + // event.preventDefault(); + + // const { token } = await call('registerUser', {}); + // Meteor.loginWithToken(token); + // }, + + const t = useTranslation(); + return ( + <div className='rc-button-group'> + <button className='rc-button rc-button--primary rc-button--small js-register'>{t('Sign_in_to_start_talking')}</button> + {isAnonymousWriteEnabled && <button className='rc-button rc-button--small js-register-anonymous'>{t('Or_talk_as_anonymous')}</button>} + </div> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerBlocked.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerBlocked.tsx new file mode 100644 index 00000000000..a6c09da0982 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerBlocked.tsx @@ -0,0 +1,8 @@ +import { MessageFooterCallout } from '@rocket.chat/ui-composer'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +export const ComposerBlocked = (): ReactElement => { + const t = useTranslation(); + return <MessageFooterCallout>{t('room_is_blocked')}</MessageFooterCallout>; +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx new file mode 100644 index 00000000000..5b5669662ab --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx @@ -0,0 +1,67 @@ +import { isOmnichannelRoom, isVoipRoom } from '@rocket.chat/core-typings'; +import React, { memo, ReactElement } from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; +import { ComposerAnonymous } from './ComposerAnonymous'; +import { ComposerBlocked } from './ComposerBlocked'; +import { ComposerJoinWithPassword } from './ComposerJoinWithPassword'; +import ComposerMessage, { ComposerMessageProps } from './ComposerMessage'; +import { ComposerOmnichannel } from './ComposerOmnichannel/ComposerOmnichannel'; +import { ComposerReadOnly } from './ComposerReadOnly'; +import { useMessageComposerIsAnonymous } from './hooks/useMessageComposerIsAnonymous'; +import { useMessageComposerIsBlocked } from './hooks/useMessageComposerIsBlocked'; +import { useMessageComposerIsReadOnly } from './hooks/useMessageComposerIsReadOnly'; + +const ComposerContainer = (props: ComposerMessageProps): ReactElement => { + const room = useRoom(); + + const mustJoinWithCode = !props.subscription && room.joinCodeRequired; + + const isAnonymous = useMessageComposerIsAnonymous(); + + const isBlockedOrBlocker = useMessageComposerIsBlocked({ subscription: props.subscription }); + + const isReadOnly = useMessageComposerIsReadOnly(props.rid, props.subscription); + + const isOmnichannel = isOmnichannelRoom(room) || isVoipRoom(room); + + if (isOmnichannel) { + return <ComposerOmnichannel {...props} />; + } + + if (isAnonymous) { + return ( + <footer className='rc-message-box footer'> + <ComposerAnonymous /> + </footer> + ); + } + + if (mustJoinWithCode) { + return ( + <footer className='rc-message-box footer'> + <ComposerJoinWithPassword /> + </footer> + ); + } + + if (isReadOnly) { + return ( + <footer className='rc-message-box footer'> + <ComposerReadOnly /> + </footer> + ); + } + + if (isBlockedOrBlocker) { + return ( + <footer className='rc-message-box footer'> + <ComposerBlocked /> + </footer> + ); + } + + return <ComposerMessage {...props} />; +}; + +export default memo(ComposerContainer); diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerJoinWithPassword.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerJoinWithPassword.tsx new file mode 100644 index 00000000000..98641fd4ccc --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerJoinWithPassword.tsx @@ -0,0 +1,54 @@ +import { TextInput } from '@rocket.chat/fuselage'; +import { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { ChangeEvent, ReactElement, useCallback, useState, FormEventHandler } from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; + +export const ComposerJoinWithPassword = (): ReactElement => { + const room = useRoom(); + const [joinCode, setJoinPassword] = useState<string>(''); + + const [error, setError] = useState<string>(''); + + const t = useTranslation(); + + const joinEndpoint = useEndpoint('POST', '/v1/channels.join'); + + const join = useCallback<FormEventHandler<HTMLElement>>( + async (e) => { + e.preventDefault(); + try { + await joinEndpoint({ + roomId: room._id, + joinCode, + }); + } catch (error: any) { + setError(error.error); + } + }, + [joinEndpoint, room._id, joinCode], + ); + + const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { + setJoinPassword(e.target.value); + setError(''); + }, []); + + return ( + <MessageFooterCallout is='form' aria-label={t('Join_with_password')} onSubmit={join}> + <MessageFooterCalloutContent>{t('you_are_in_preview')}</MessageFooterCalloutContent> + <MessageFooterCalloutContent> + <TextInput + error={error} + value={joinCode} + onChange={handleChange} + placeholder={t('you_are_in_preview_please_insert_the_password')} + /> + </MessageFooterCalloutContent> + <MessageFooterCalloutAction type='submit' disabled={Boolean(!joinCode)}> + {t('Join_with_password')} + </MessageFooterCalloutAction> + </MessageFooterCallout> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/ComposerContainer.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx similarity index 77% rename from apps/meteor/client/views/room/components/body/ComposerContainer.tsx rename to apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx index 2b7cd16de7a..3106ed834c9 100644 --- a/apps/meteor/client/views/room/components/body/ComposerContainer.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx @@ -5,20 +5,20 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import React, { memo, ReactElement, useCallback, useEffect, useRef } from 'react'; -import { ChatMessages } from '../../../../../app/ui'; -import { RoomManager } from '../../../../../app/ui-utils/client'; -import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import ComposerSkeleton from '../../Room/ComposerSkeleton'; +import { ChatMessages } from '../../../../../../app/ui'; +import { RoomManager } from '../../../../../../app/ui-utils/client'; +import { useEmbeddedLayout } from '../../../../../hooks/useEmbeddedLayout'; +import { useReactiveValue } from '../../../../../hooks/useReactiveValue'; +import ComposerSkeleton from '../../../Room/ComposerSkeleton'; -type ComposerContainerProps = { +export type ComposerMessageProps = { rid: IRoom['_id']; subscription?: ISubscription; chatMessagesInstance: ChatMessages; onResize?: () => void; }; -const ComposerContainer = ({ rid, subscription, chatMessagesInstance, onResize }: ComposerContainerProps): ReactElement => { +const ComposerMessage = ({ rid, subscription, chatMessagesInstance, onResize }: ComposerMessageProps): ReactElement => { const isLayoutEmbedded = useEmbeddedLayout(); const showFormattingTips = useSetting('Message_ShowFormattingTips') as boolean; @@ -88,9 +88,9 @@ const ComposerContainer = ({ rid, subscription, chatMessagesInstance, onResize } [rid, chatMessagesInstance], ); - const subscriptionReady = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(rid)?.streamActive ?? false, [rid])); + const publicationReady = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(rid)?.streamActive ?? false, [rid])); - if (!subscriptionReady) { + if (!publicationReady) { return ( <footer className='footer'> <ComposerSkeleton /> @@ -101,4 +101,4 @@ const ComposerContainer = ({ rid, subscription, chatMessagesInstance, onResize } return <footer ref={footerRef} className='footer' />; }; -export default memo(ComposerContainer); +export default memo(ComposerMessage); diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx new file mode 100644 index 00000000000..064c72b8ac2 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx @@ -0,0 +1,57 @@ +import { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { MessageFooterCallout } from '@rocket.chat/ui-composer'; +import { useStream, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useEffect, useState } from 'react'; + +import { useOmnichannelRoom, useUserIsSubscribed } from '../../../../contexts/RoomContext'; +import ComposerMessage, { ComposerMessageProps } from '../ComposerMessage'; +import { ComposerOmnichannelInquiry } from './ComposerOmnichannelInquiry'; +import { ComposerOmnichannelJoin } from './ComposerOmnichannelJoin'; +import { ComposerOmnichannelOnHold } from './ComposerOmnichannelOnHold'; + +export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement => { + const { queuedAt, servedBy, _id, open, onHold } = useOmnichannelRoom(); + + const isSubscribed = useUserIsSubscribed(); + const [isInquired, setIsInquired] = useState(() => !servedBy && queuedAt); + + const subscribeToRoom = useStream('room-data'); + + const t = useTranslation(); + + useEffect(() => { + subscribeToRoom(_id, (entry: IOmnichannelRoom) => { + setIsInquired(!entry.servedBy && entry.queuedAt); + }); + }, [_id, subscribeToRoom]); + + useEffect(() => { + setIsInquired(!servedBy && queuedAt); + }, [queuedAt, servedBy, _id]); + + if (!open) { + return ( + <footer className='rc-message-box footer'> + <MessageFooterCallout>{t('This_conversation_is_already_closed')}</MessageFooterCallout> + </footer> + ); + } + + if (onHold) { + return <ComposerOmnichannelOnHold />; + } + + if (isInquired) { + return <ComposerOmnichannelInquiry />; + } + + if (!isSubscribed) { + return <ComposerOmnichannelJoin />; + } + + return ( + <> + <ComposerMessage {...props} /> + </> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx new file mode 100644 index 00000000000..a0c87608166 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx @@ -0,0 +1,42 @@ +import { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import { useEndpoint, useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React, { ReactElement } from 'react'; + +import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; + +export const ComposerOmnichannelInquiry = (): ReactElement => { + const dispatchToastMessage = useToastMessageDispatch(); + + const room = useOmnichannelRoom(); + const getInquire = useEndpoint('GET', `/v1/livechat/inquiries.getOne`); + const result = useQuery(['inquire', room._id], () => + getInquire({ + roomId: room._id, + }), + ); + const takeInquiry = useMethod('livechat:takeInquiry'); + + const handleTakeInquiry = async (): Promise<void> => { + if (!result.isSuccess) { + return; + } + try { + await takeInquiry(result.data.inquiry._id); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + const t = useTranslation(); + return ( + <footer className='rc-message-box footer'> + <MessageFooterCallout aria-busy={result.isLoading}> + <MessageFooterCalloutContent>{t('you_are_in_preview_mode_of_incoming_livechat')}</MessageFooterCalloutContent> + <MessageFooterCalloutAction disabled={result.isLoading} onClick={handleTakeInquiry}> + {t('Take_it')} + </MessageFooterCalloutAction> + </MessageFooterCallout> + </footer> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelJoin.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelJoin.tsx new file mode 100644 index 00000000000..6bb8f69df88 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelJoin.tsx @@ -0,0 +1,34 @@ +import { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; + +export const ComposerOmnichannelJoin = (): ReactElement => { + const room = useOmnichannelRoom(); + const join = useEndpoint('GET', `/v1/livechat/room.join`); + + const dispatchToastMessage = useToastMessageDispatch(); + + const t = useTranslation(); + return ( + <footer className='rc-message-box footer'> + <MessageFooterCallout> + <MessageFooterCalloutContent>{t('room_is_read_only')}</MessageFooterCalloutContent> + <MessageFooterCalloutAction + onClick={async (): Promise<void> => { + try { + await join({ + roomId: room._id, + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }} + > + {t('Join')} + </MessageFooterCalloutAction> + </MessageFooterCallout> + </footer> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx new file mode 100644 index 00000000000..a0fde3c5874 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx @@ -0,0 +1,19 @@ +import { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; + +export const ComposerOmnichannelOnHold = (): ReactElement => { + const resume = useMethod('livechat:resumeOnHold'); + const room = useOmnichannelRoom(); + const t = useTranslation(); + return ( + <footer className='rc-message-box footer'> + <MessageFooterCallout> + <MessageFooterCalloutContent>{t('chat_on_hold_due_to_inactivity')}</MessageFooterCalloutContent> + <MessageFooterCalloutAction onClick={(): Promise<unknown> => resume(room._id)}>{t('Resume')}</MessageFooterCalloutAction> + </MessageFooterCallout> + </footer> + ); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/index.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/index.tsx new file mode 100644 index 00000000000..6c2ea9a42b0 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/index.tsx @@ -0,0 +1 @@ +export * from './ComposerOmnichannel'; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerReadOnly.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerReadOnly.tsx new file mode 100644 index 00000000000..ec7b2e7524e --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerReadOnly.tsx @@ -0,0 +1,8 @@ +import { MessageFooterCallout } from '@rocket.chat/ui-composer'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +export const ComposerReadOnly = (): ReactElement => { + const t = useTranslation(); + return <MessageFooterCallout>{t('room_is_read_only')}</MessageFooterCallout>; +}; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerUsersAction/ComposerUsersAction.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerUsersAction/ComposerUsersAction.tsx new file mode 100644 index 00000000000..3edea45a155 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerUsersAction/ComposerUsersAction.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const ComposerUsersAction = () => <>asd</>; diff --git a/apps/meteor/client/views/room/components/body/composer/MessageComposer.spec.ts b/apps/meteor/client/views/room/components/body/composer/MessageComposer.spec.ts new file mode 100644 index 00000000000..3ec8fbec36d --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/MessageComposer.spec.ts @@ -0,0 +1,14 @@ +// Anonymous read enabled: true +// Anonymous write enabled: true + +// Room public: joinCodeRequired: true +// Room public: joinCodeRequired: false + +// federation: enabled + +// read only: true and has no permission to post +// read only: true but has permission to post + +// omnichannel: onhold +// omnichannel: custom composers +// omnichannel: inquiry diff --git a/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerCanJoin.ts b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerCanJoin.ts new file mode 100644 index 00000000000..d973e025034 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerCanJoin.ts @@ -0,0 +1,8 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useCallback } from 'react'; + +import { useReactiveValue } from '../../../../../../hooks/useReactiveValue'; +import { roomCoordinator } from '../../../../../../lib/rooms/roomCoordinator'; + +export const useMessageComposerCanJoin = (rid: IRoom['_id']): boolean => + Boolean(useReactiveValue(useCallback(() => Meteor.userId() && roomCoordinator.getRoomDirectivesById(rid)?.showJoinLink(rid), [rid]))); diff --git a/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsAnonymous.ts b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsAnonymous.ts new file mode 100644 index 00000000000..f12f2c9bdd4 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsAnonymous.ts @@ -0,0 +1,13 @@ +import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; + +export const useMessageComposerIsAnonymous = (): boolean => { + const isAnonymousReadEnabled = useSetting('Accounts_AllowAnonymousRead'); + const isAnonymousWriteEnabled = useSetting('Accounts_AllowAnonymousWrite'); + + const uid = useUserId(); + + if (!uid && !isAnonymousReadEnabled && !isAnonymousWriteEnabled) { + throw new Error('Anonymous access is disabled'); + } + return Boolean(!uid); +}; diff --git a/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsBlocked.ts b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsBlocked.ts new file mode 100644 index 00000000000..6f0ab39f206 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsBlocked.ts @@ -0,0 +1,18 @@ +import { isDirectMessageRoom, ISubscription } from '@rocket.chat/core-typings'; + +import { useRoom } from '../../../../contexts/RoomContext'; + +export const useMessageComposerIsBlocked = ({ subscription }: { subscription?: ISubscription }): boolean => { + const room = useRoom(); + + if (!isDirectMessageRoom(room)) { + return false; + } + if (!subscription) { + return false; + } + // over DM's is possible to block or being blocked by the other user + const isBlocked = Boolean(subscription.blocked); + const isBlocker = Boolean(subscription.blocker); + return isBlocked || isBlocker; +}; diff --git a/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsReadOnly.ts b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsReadOnly.ts new file mode 100644 index 00000000000..8edb3dc15ad --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/hooks/useMessageComposerIsReadOnly.ts @@ -0,0 +1,20 @@ +import { ISubscription, IUser } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; +import { useCallback } from 'react'; + +import { useReactiveValue } from '../../../../../../hooks/useReactiveValue'; +import { roomCoordinator } from '../../../../../../lib/rooms/roomCoordinator'; + +export const useMessageComposerIsReadOnly = (rid: string, subscription?: ISubscription): boolean => { + const [isReadOnly, isArchived] = useReactiveValue( + useCallback( + () => [ + roomCoordinator.readOnly(rid, Meteor.users.findOne(Meteor.userId() as string, { fields: { username: 1 } }) as IUser), + roomCoordinator.archived(rid) || Boolean(subscription && subscription.t === 'd' && subscription.archived), + ], + [rid, subscription], + ), + ); + + return Boolean(isReadOnly || isArchived); +}; diff --git a/apps/meteor/client/views/room/contexts/RoomContext.ts b/apps/meteor/client/views/room/contexts/RoomContext.ts index c2ebf16b8e4..2d138165db3 100644 --- a/apps/meteor/client/views/room/contexts/RoomContext.ts +++ b/apps/meteor/client/views/room/contexts/RoomContext.ts @@ -12,6 +12,16 @@ export type RoomContextValue = { export const RoomContext = createContext<RoomContextValue | null>(null); +export const useUserIsSubscribed = (): boolean => { + const context = useContext(RoomContext); + + if (!context) { + throw new Error('use useRoom only inside opened rooms'); + } + + return context.subscribed ?? false; +}; + export const useRoom = (): IRoom => { const { room } = useContext(RoomContext) || {}; diff --git a/apps/meteor/definition/IRoomTypeConfig.ts b/apps/meteor/definition/IRoomTypeConfig.ts index 4b2689996d1..d039acacd60 100644 --- a/apps/meteor/definition/IRoomTypeConfig.ts +++ b/apps/meteor/definition/IRoomTypeConfig.ts @@ -53,8 +53,10 @@ export interface IRoomTypeConfig { label?: string; route?: IRoomTypeRouteConfig; customTemplate?: string; - notSubscribedTpl?: string; - readOnlyTpl?: string; + /** @deprecated */ + notSubscribedTpl?: 'livechatNotSubscribed'; + /** @deprecated */ + readOnlyTpl?: 'ComposerNotAvailablePhoneCalls' | 'livechatReadOnly'; } export interface IRoomTypeClientDirectives { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index ec59c061ba0..0de11be17c0 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -227,6 +227,7 @@ "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-composer": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", "@rocket.chat/ui-kit": "next", "@rocket.chat/ui-video-conf": "workspace:^", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index cab1af193c6..5b0a79ebc00 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2574,6 +2574,8 @@ "italics": "italics", "Items_per_page:": "Items per page:", "Job_Title": "Job Title", + "Join": "Join", + "Join_with_password": "Join with password", "Join_audio_call": "Join audio call", "Join_call": "Join call", "Join_Chat": "Join Chat", @@ -3944,6 +3946,7 @@ "Restart_the_server": "Restart The Server", "restart-server": "Restart the server", "restart-server_description": "Permission to restart the server", + "Resume": "Resume", "Retail": "Retail", "Retention_setting_changed_successfully": "Retention policy setting changed successfully", "RetentionPolicy": "Retention Policy", @@ -5291,6 +5294,8 @@ "You_users_and_more_Reacted_with": "You, __users__ and __counter__ more reacted with __emoji__", "You_are_converting_team_to_channel": "You are converting this Team to a Channel.", "you_are_in_preview_mode_of": "You are in preview mode of channel #<strong>__room_name__</strong>", + "you_are_in_preview": "You are in preview mode", + "you_are_in_preview_please_insert_the_password": "Please insert the password", "you_are_in_preview_mode_of_incoming_livechat": "You are in preview mode of this chat", "You_are_logged_in_as": "You are logged in as", "You_are_not_authorized_to_view_this_page": "You are not authorized to view this page.", diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 0142b3922c5..23d05ba374b 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -176,6 +176,6 @@ export class HomeContent { } get takeOmnichannelChatButton(): Locator { - return this.page.locator('button.rc-button >> text=Take it!'); + return this.page.locator('role=button[name="Take it!"]'); } } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 2e1c314232d..6f3876ba0ae 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -25,6 +25,7 @@ export interface IRoom extends IRocketChatRecord { broadcast?: true; featured?: true; announcement?: string; + joinCodeRequired?: boolean; announcementDetails?: { style?: string; }; @@ -173,7 +174,8 @@ export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featur priorityId: any; livechatData: any; queuedAt?: Date; - status?: 'queued'; // TODO: missing types for this + + status?: 'queued' | 'taken' | 'ready'; // TODO: missing types for this ts: Date; label?: string; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 439ba470b10..7b4a176baf8 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -13,6 +13,7 @@ import type { IRoom, ISetting, ILivechatPriority, + ILivechatInquiryRecord, } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; @@ -1054,6 +1055,11 @@ const GETOmnichannelContactSearchSchema = { export const isGETOmnichannelContactSearchProps = ajv.compile<GETOmnichannelContactSearchProps>(GETOmnichannelContactSearchSchema); export type OmnichannelEndpoints = { + '/v1/livechat/inquiries.getOne': { + GET: (params: { roomId: string }) => { + inquiry: ILivechatInquiryRecord; + }; + }; '/v1/livechat/appearance': { GET: () => { appearance: ISetting[]; diff --git a/packages/ui-composer/.eslintrc.json b/packages/ui-composer/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/packages/ui-composer/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/ui-composer/.storybook/main.js b/packages/ui-composer/.storybook/main.js new file mode 100644 index 00000000000..4b6ae93751a --- /dev/null +++ b/packages/ui-composer/.storybook/main.js @@ -0,0 +1,12 @@ +module.exports = { + "stories": [ + "../src/**/*.stories.mdx", + "../src/**/*.stories.@(js|jsx|ts|tsx)" + ], + "addons": [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions" + ], + "framework": "@storybook/react" +} diff --git a/packages/ui-composer/.storybook/preview.js b/packages/ui-composer/.storybook/preview.js new file mode 100644 index 00000000000..abd704f7951 --- /dev/null +++ b/packages/ui-composer/.storybook/preview.js @@ -0,0 +1,25 @@ +import '../../../apps/meteor/app/theme/client/main.css'; +import 'highlight.js/styles/github.css'; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +} + +export const decorators = [ + (Story) => ( + <div className="rc-old"> + <style>{` + body { + background-color: white; + } + `}</style> + <Story /> + </div> + ) +]; diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json new file mode 100644 index 00000000000..6cd3858d674 --- /dev/null +++ b/packages/ui-composer/package.json @@ -0,0 +1,46 @@ +{ + "name": "@rocket.chat/ui-composer", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@babel/core": "~7.18.13", + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/fuselage": "next", + "@rocket.chat/icons": "next", + "@storybook/addon-actions": "~6.5.12", + "@storybook/addon-docs": "~6.5.12", + "@storybook/addon-essentials": "~6.5.12", + "@storybook/builder-webpack4": "~6.5.12", + "@storybook/manager-webpack4": "~6.5.12", + "@storybook/react": "~6.5.12", + "@storybook/testing-library": "~0.0.13", + "@types/babel__core": "^7", + "@types/jest": "^27.4.1", + "eslint": "^8.22.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-storybook": "^0.6.4", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typescript": "~4.5.5" + }, + "peerDependencies": { + "@rocket.chat/fuselage": "*", + "@rocket.chat/icons": "*", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "jest": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", + "storybook": "start-storybook -p 6006" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx b/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx new file mode 100644 index 00000000000..2075d1fe376 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposer.stories.tsx @@ -0,0 +1,69 @@ +import { Button } from '@rocket.chat/fuselage'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +import '@rocket.chat/icons/dist/rocketchat.css'; +import { + MessageComposer, + MessageComposerAction, + MessageComposerToolbarActions, + MessageComposerInput, + MessageComposerToolbar, + MessageComposerActionsDivider, + MessageComposerToolbarSubmit, + MessageComposerSkeleton, +} from '.'; + +export default { + title: 'Components/MessageComposer', + component: MessageComposer, +} as ComponentMeta<typeof MessageComposer>; + +export const messageComposer: ComponentStory<typeof MessageComposer> = () => ( + <MessageComposer> + <MessageComposerInput placeholder='Text' /> + <MessageComposerToolbar> + <MessageComposerToolbarActions> + <MessageComposerAction icon='emoji' /> + <MessageComposerActionsDivider /> + <MessageComposerAction icon='bold' /> + <MessageComposerAction icon='italic' /> + <MessageComposerAction icon='underline' /> + <MessageComposerAction icon='strike' /> + <MessageComposerAction icon='code' /> + <MessageComposerAction icon='arrow-return' /> + <MessageComposerActionsDivider /> + <MessageComposerAction icon='mic' /> + <MessageComposerAction icon='clip' /> + </MessageComposerToolbarActions> + </MessageComposerToolbar> + </MessageComposer> +); + +export const messageComposerWithSubmitActions: ComponentStory<typeof MessageComposer> = () => ( + <MessageComposer> + <MessageComposerInput placeholder='Text' /> + <MessageComposerToolbar> + <MessageComposerToolbarActions> + <MessageComposerAction icon='emoji' /> + <MessageComposerActionsDivider /> + <MessageComposerAction icon='bold' /> + <MessageComposerAction icon='italic' /> + <MessageComposerAction icon='underline' /> + <MessageComposerAction icon='strike' /> + <MessageComposerAction icon='code' /> + <MessageComposerAction icon='arrow-return' /> + <MessageComposerActionsDivider /> + <MessageComposerAction icon='mic' /> + <MessageComposerAction icon='clip' /> + </MessageComposerToolbarActions> + <MessageComposerToolbarSubmit> + <Button small>Preview</Button> + <Button primary small> + Send + </Button> + </MessageComposerToolbarSubmit> + </MessageComposerToolbar> + </MessageComposer> +); + +export const messageComposerLoading: ComponentStory<typeof MessageComposer> = () => <MessageComposerSkeleton />; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposer.tsx b/packages/ui-composer/src/MessageComposer/MessageComposer.tsx new file mode 100644 index 00000000000..84a460909a9 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposer.tsx @@ -0,0 +1,26 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; +import { forwardRef } from 'react'; + +type MessageComposerProps = Omit<HTMLAttributes<HTMLElement>, 'is'> & { + children: ReactNode; + is?: React.ElementType<any>; + variant?: 'default' | 'error'; +}; + +const MessageComposer = forwardRef<HTMLElement, MessageComposerProps>( + (props, ref): ReactElement => ( + <Box + ref={ref} + role='group' + borderWidth={2} + borderColor='neutral-500' + borderRadius='x4' + display='flex' + flexDirection='column' + {...props} + /> + ), +); + +export default MessageComposer; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerAction.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerAction.tsx new file mode 100644 index 00000000000..5233f78499c --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerAction.tsx @@ -0,0 +1,6 @@ +import { IconButton } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; + +const MessageComposerAction = (props: ComponentProps<typeof IconButton>): ReactElement => <IconButton small {...props} />; + +export default MessageComposerAction; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerActionsDivider.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerActionsDivider.tsx new file mode 100644 index 00000000000..6fec42e7e12 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerActionsDivider.tsx @@ -0,0 +1,8 @@ +import { Divider } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; + +const MessageComposerActionsDivider = ({ height = 'x20', ...props }: ComponentProps<typeof Divider>): ReactElement => ( + <Divider mi='x4' borderColor='neutral-500' mb={0} backgroundColor='neutral-500' height={height} {...props} /> +); + +export default MessageComposerActionsDivider; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerIcon.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerIcon.tsx new file mode 100644 index 00000000000..4ca0dec4ef4 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerIcon.tsx @@ -0,0 +1,9 @@ +import type { Keys } from '@rocket.chat/icons'; +import { Icon } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, ReactElement } from 'react'; + +const MessageComposerIcon = ({ name, ...props }: { name: Keys } & Omit<HTMLAttributes<HTMLElement>, 'is'>): ReactElement => ( + <Icon name={name} size='x20' mie='x4' {...props} /> +); + +export default MessageComposerIcon; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx new file mode 100644 index 00000000000..a3eba1b340a --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx @@ -0,0 +1,6 @@ +import { TextInput } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; + +const MessageComposerInput = (props: ComponentProps<typeof TextInput>): ReactElement => <TextInput {...props} borderWidth={0} />; + +export default MessageComposerInput; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerSkeleton.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerSkeleton.tsx new file mode 100644 index 00000000000..736f04d6af8 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerSkeleton.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +import { + MessageComposer, + MessageComposerActionsDivider, + MessageComposerToolbar, + MessageComposerToolbarActions, + MessageComposerToolbarSubmit, +} from '.'; + +const MessageComposerSkeleton = (): ReactElement => ( + <MessageComposer> + <Skeleton p='x4' m='x8' /> + <MessageComposerToolbar> + <MessageComposerToolbarActions> + <Skeleton p='x4' pi='x6' /> + <Skeleton p='x4' pi='x6' /> + <MessageComposerActionsDivider /> + <Skeleton p='x4' pi='x6' /> + <Skeleton p='x4' pi='x6' /> + </MessageComposerToolbarActions> + <MessageComposerToolbarSubmit> + <Skeleton p='x4' width={60} /> + <Skeleton p='x4' width={60} /> + </MessageComposerToolbarSubmit> + </MessageComposerToolbar> + </MessageComposer> +); + +export default MessageComposerSkeleton; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx new file mode 100644 index 00000000000..7acac8199d5 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerToolbar.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; + +const MessageComposerToolbar = (props: ComponentProps<typeof Box>): ReactElement => ( + <Box backgroundColor='neutral-200' pb='x4' pie='x4' display='flex' justifyContent='space-between' role='toolbar' {...props} /> +); + +export default MessageComposerToolbar; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx new file mode 100644 index 00000000000..7c72d60fb06 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarActions.tsx @@ -0,0 +1,6 @@ +import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +const MessageComposerToolbarActions = ({ ...props }): ReactElement => <ButtonGroup role='toolbar' small mis='x4' {...props} />; + +export default MessageComposerToolbarActions; diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerToolbarSubmit.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarSubmit.tsx new file mode 100644 index 00000000000..4ae5f917219 --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/MessageComposerToolbarSubmit.tsx @@ -0,0 +1,6 @@ +import { ButtonGroup } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; + +const MessageComposerToolbarSubmit = (props: ComponentProps<typeof ButtonGroup>): ReactElement => <ButtonGroup small {...props} />; + +export default MessageComposerToolbarSubmit; diff --git a/packages/ui-composer/src/MessageComposer/index.ts b/packages/ui-composer/src/MessageComposer/index.ts new file mode 100644 index 00000000000..bca980ab77a --- /dev/null +++ b/packages/ui-composer/src/MessageComposer/index.ts @@ -0,0 +1,21 @@ +import MessageComposer from './MessageComposer'; +import MessageComposerAction from './MessageComposerAction'; +import MessageComposerActionsDivider from './MessageComposerActionsDivider'; +import MessageComposerInput from './MessageComposerInput'; +import MessageComposerSkeleton from './MessageComposerSkeleton'; +import MessageComposerToolbar from './MessageComposerToolbar'; +import MessageComposerToolbarActions from './MessageComposerToolbarActions'; +import MessageComposerToolbarSubmit from './MessageComposerToolbarSubmit'; +import MessageComposerIcon from './MessageComposerIcon'; + +export { + MessageComposer, + MessageComposerAction, + MessageComposerActionsDivider, + MessageComposerInput, + MessageComposerToolbar, + MessageComposerToolbarActions, + MessageComposerToolbarSubmit, + MessageComposerSkeleton, + MessageComposerIcon, +}; diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.stories.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.stories.tsx new file mode 100644 index 00000000000..ea66559a496 --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.stories.tsx @@ -0,0 +1,53 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +import '@rocket.chat/icons/dist/rocketchat.css'; +import { MessageFooterCallout, MessageFooterCalloutAction } from '.'; +import MessageComposer from '../MessageComposer/MessageComposer'; +import MessageComposerIcon from '../MessageComposer/MessageComposerIcon'; +import MessageFooterCalloutDivider from './MessageFooterCalloutDivider'; +import MessageFooterCalloutContent from './MessageFooterCalloutContent'; + +export default { + title: 'Components/MessageComposer/Locked', + component: MessageComposer, +} as ComponentMeta<typeof MessageComposer>; + +export const messageComposerBlocked: ComponentStory<typeof MessageComposer> = () => ( + <MessageFooterCallout> + <MessageComposerIcon name='burger' /> + Feedback text + </MessageFooterCallout> +); + +export const messageComposerBlockedLargeText: ComponentStory<typeof MessageComposer> = () => ( + <MessageFooterCallout> + <MessageComposerIcon name='burger' /> + <MessageFooterCalloutContent> + Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback + text Feedback text Feedback text Feedback text Feedback text Feedback text text Feedback text Feedback text Feedback text Feedback + text Feedback text text Feedback text Feedback text Feedback text Feedback text Feedback text text Feedback text Feedback text + Feedback text Feedback text Feedback text + </MessageFooterCalloutContent> + <MessageFooterCalloutAction onClick={() => undefined}>Button</MessageFooterCalloutAction> + </MessageFooterCallout> +); + +export const messageComposerBlockedLargeTextDashed: ComponentStory<typeof MessageComposer> = () => ( + <MessageFooterCallout dashed> + <MessageComposerIcon name='burger' /> + <MessageFooterCalloutContent> + Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback text Feedback + text Feedback text Feedback text Feedback text Feedback text Feedback text text Feedback text Feedback text Feedback text Feedback + text Feedback text text Feedback text Feedback text Feedback text Feedback text Feedback text text Feedback text Feedback text + Feedback text Feedback text Feedback text + </MessageFooterCalloutContent> + <MessageFooterCalloutAction onClick={() => undefined}>Button</MessageFooterCalloutAction> + </MessageFooterCallout> +); + +export const messageFooterCalloutAction: ComponentStory<typeof MessageComposer> = () => ( + <MessageFooterCallout> + Feedback text <MessageFooterCalloutDivider /> + <MessageFooterCalloutAction onClick={() => undefined}>Button</MessageFooterCalloutAction> + </MessageFooterCallout> +); diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.tsx new file mode 100644 index 00000000000..a71306dec86 --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCallout.tsx @@ -0,0 +1,34 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; +import { forwardRef } from 'react'; + +const MessageFooterCallout = forwardRef< + HTMLElement, + Omit<HTMLAttributes<HTMLElement>, 'is'> & { + children: ReactNode; + is?: React.ElementType<any>; + variant?: 'default' | 'error'; + dashed?: boolean; + } +>( + ({ dashed, ...props }, ref): ReactElement => ( + <Box + ref={ref} + {...(dashed && { + borderStyle: 'dashed', + })} + display='flex' + borderWidth={2} + borderColor='neutral-500' + borderRadius='x4' + p='x8' + backgroundColor='neutral-200' + alignItems='center' + minHeight='x48' + justifyContent='center' + {...props} + /> + ), +); + +export default MessageFooterCallout; diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutAction.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutAction.tsx new file mode 100644 index 00000000000..f4739804baf --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutAction.tsx @@ -0,0 +1,9 @@ +import { Button } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; +import { forwardRef } from 'react'; + +const MessageFooterCalloutAction = forwardRef<HTMLButtonElement, ComponentProps<typeof Button>>( + (props, ref): ReactElement => <Button mi='x4' ref={ref} primary small flexShrink={0} {...props} />, +); + +export default MessageFooterCalloutAction; diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx new file mode 100644 index 00000000000..939002a30da --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx @@ -0,0 +1,12 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement, ReactNode } from 'react'; +import { forwardRef } from 'react'; + +const MessageFooterCalloutContent = forwardRef< + HTMLButtonElement, + { + children: ReactNode; + } +>((props, ref): ReactElement => <Box mi='x4' ref={ref} flexWrap='wrap' flexShrink={1} {...props} />); + +export default MessageFooterCalloutContent; diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutDivider.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutDivider.tsx new file mode 100644 index 00000000000..fcfc8ed57bd --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutDivider.tsx @@ -0,0 +1,9 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import { forwardRef } from 'react'; + +const MessageFooterCalloutDivider = forwardRef<HTMLButtonElement>( + (props, ref): ReactElement => <Box is='hr' ref={ref} borderInlineStart='1px solid' mi='x4' flexShrink={0} {...props} />, +); + +export default MessageFooterCalloutDivider; diff --git a/packages/ui-composer/src/MessageFooterCallout/index.ts b/packages/ui-composer/src/MessageFooterCallout/index.ts new file mode 100644 index 00000000000..2aa008826cf --- /dev/null +++ b/packages/ui-composer/src/MessageFooterCallout/index.ts @@ -0,0 +1,6 @@ +import MessageFooterCallout from './MessageFooterCallout'; +import MessageFooterCalloutAction from './MessageFooterCalloutAction'; +import MessageFooterCalloutDivider from './MessageFooterCalloutDivider'; +import MessageFooterCalloutContent from './MessageFooterCalloutContent'; + +export { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutDivider, MessageFooterCalloutContent }; diff --git a/packages/ui-composer/src/index.ts b/packages/ui-composer/src/index.ts new file mode 100644 index 00000000000..bd90595be83 --- /dev/null +++ b/packages/ui-composer/src/index.ts @@ -0,0 +1,2 @@ +export * from './MessageComposer'; +export * from './MessageFooterCallout'; diff --git a/packages/ui-composer/tsconfig.json b/packages/ui-composer/tsconfig.json new file mode 100644 index 00000000000..455edb8149c --- /dev/null +++ b/packages/ui-composer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index ed9c9b14509..5ba69c41bd9 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -225,6 +225,8 @@ export interface ServerMethods { 'checkRegistrationSecretURL': (hash: string) => boolean; 'livechat:changeLivechatStatus': (params?: void | { status?: string; agentId?: string }) => unknown; 'livechat:saveAgentInfo': (_id: string, agentData: unknown, agentDepartments: unknown) => unknown; + 'livechat:takeInquiry': (inquiryId: string) => unknown; + 'livechat:resumeOnHold': (roomId: string) => unknown; 'autoTranslate.getProviderUiMetadata': () => Record<string, { name: string; displayName: string }>; 'autoTranslate.getSupportedLanguages': (language: string) => ISupportedLanguage[]; 'spotlight': ( diff --git a/yarn.lock b/yarn.lock index 0e96d0f2727..c875ce41cde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -137,6 +137,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:~7.18.13": + version: 7.18.13 + resolution: "@babel/core@npm:7.18.13" + dependencies: + "@ampproject/remapping": ^2.1.0 + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.18.13 + "@babel/helper-compilation-targets": ^7.18.9 + "@babel/helper-module-transforms": ^7.18.9 + "@babel/helpers": ^7.18.9 + "@babel/parser": ^7.18.13 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.18.13 + "@babel/types": ^7.18.13 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.1 + semver: ^6.3.0 + checksum: c7ee5b2c10bc5b0325e31fb5da4cb4bc03f9d5f5c00ec3481a018917bcc6b7b040de0690c606a424f57e5fc26d218d64e7718d0e5d7d8614d39c8cd898fab9b3 + languageName: node + linkType: hard + "@babel/eslint-parser@npm:^7.18.9": version: 7.18.9 resolution: "@babel/eslint-parser@npm:7.18.9" @@ -162,7 +185,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.19.0": +"@babel/generator@npm:^7.18.13, @babel/generator@npm:^7.19.0": version: 7.19.0 resolution: "@babel/generator@npm:7.19.0" dependencies: @@ -619,6 +642,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.18.13, @babel/parser@npm:^7.19.0": + version: 7.19.0 + resolution: "@babel/parser@npm:7.19.0" + bin: + parser: ./bin/babel-parser.js + checksum: af86d829bfeb60e0dcf54a43489c2514674b6c8d9bb24cf112706772125752fcd517877ad30501d533fa85f70a439d02eebeec3be9c2e95499853367184e0da7 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -2037,6 +2069,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.18.13": + version: 7.19.0 + resolution: "@babel/traverse@npm:7.19.0" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.19.0 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.19.0 + "@babel/types": ^7.19.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: dcbd1316c9f4bf3cefee45b6f5194590563aa5d123500a60d3c8d714bef279205014c8e599ebafc469967199a7622e1444cd0235c16d4243da437e3f1281771e + languageName: node + linkType: hard + "@babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1": version: 7.19.1 resolution: "@babel/traverse@npm:7.19.1" @@ -2065,7 +2115,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.18.10, @babel/types@npm:^7.19.0": +"@babel/types@npm:^7.18.10, @babel/types@npm:^7.18.13, @babel/types@npm:^7.19.0": version: 7.19.0 resolution: "@babel/types@npm:7.19.0" dependencies: @@ -2424,6 +2474,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^1.3.2": + version: 1.3.2 + resolution: "@eslint/eslintrc@npm:1.3.2" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.4.0 + globals: ^13.15.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: 2074dca47d7e1c5c6323ff353f690f4b25d3ab53fe7d27337e2592d37a894cf60ca0e85ca66b50ff2db0bc7e630cc1e9c7347d65bb185b61416565584c38999c + languageName: node + linkType: hard + "@faker-js/faker@npm:^6.3.1": version: 6.3.1 resolution: "@faker-js/faker@npm:6.3.1" @@ -2582,6 +2649,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + "@humanwhocodes/object-schema@npm:^1.2.0, @humanwhocodes/object-schema@npm:^1.2.1": version: 1.2.1 resolution: "@humanwhocodes/object-schema@npm:1.2.1" @@ -5833,9 +5907,9 @@ __metadata: linkType: soft "@rocket.chat/icons@npm:next": - version: 0.32.0-dev.133 - resolution: "@rocket.chat/icons@npm:0.32.0-dev.133" - checksum: 7355dcad41052d849ba739efed5c4b05a76a09ea23db542a5b9cca14730e2b4a24f9502e161e60eebf876e4e3e65ceeb6203bea5d579235a00a2ed04f5c62531 + version: 0.32.0-dev.130 + resolution: "@rocket.chat/icons@npm:0.32.0-dev.130" + checksum: e7dbbf45e7b7eeb81670746d2440cb55bbe750fe432f2ac81f8437e02c08ee9be5596828d65392eca2664b667e2c608b417daf0db70277fdd51efda33726cbe4 languageName: node linkType: hard @@ -6016,6 +6090,7 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": next "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-composer": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": next "@rocket.chat/ui-video-conf": "workspace:^" @@ -6525,6 +6600,38 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/ui-composer@workspace:^, @rocket.chat/ui-composer@workspace:packages/ui-composer": + version: 0.0.0-use.local + resolution: "@rocket.chat/ui-composer@workspace:packages/ui-composer" + dependencies: + "@babel/core": ~7.18.13 + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/fuselage": next + "@rocket.chat/icons": next + "@storybook/addon-actions": ~6.5.12 + "@storybook/addon-docs": ~6.5.12 + "@storybook/addon-essentials": ~6.5.12 + "@storybook/builder-webpack4": ~6.5.12 + "@storybook/manager-webpack4": ~6.5.12 + "@storybook/react": ~6.5.12 + "@storybook/testing-library": ~0.0.13 + "@types/babel__core": ^7 + "@types/jest": ^27.4.1 + eslint: ^8.22.0 + eslint-plugin-react: ^7.30.1 + eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-storybook: ^0.6.4 + jest: ^27.5.1 + ts-jest: ^27.1.4 + typescript: ~4.5.5 + peerDependencies: + "@rocket.chat/fuselage": "*" + "@rocket.chat/icons": "*" + react: ^17.0.2 + react-dom: ^17.0.2 + languageName: unknown + linkType: soft + "@rocket.chat/ui-contexts@workspace:^, @rocket.chat/ui-contexts@workspace:packages/ui-contexts, @rocket.chat/ui-contexts@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/ui-contexts@workspace:packages/ui-contexts" @@ -6764,6 +6871,41 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-actions@npm:6.5.12, @storybook/addon-actions@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-actions@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + polished: ^4.2.2 + prop-types: ^15.7.2 + react-inspector: ^5.1.0 + regenerator-runtime: ^0.13.7 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + uuid-browser: ^3.1.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 94f433a6b0956e4301e5b46c68eb56f6a9b01b5ec314099611d584542369d1aec4878a5353052f963e462b1b5f3ce74070f0d03eadf30e275b0b88ef7b713dd8 + languageName: node + linkType: hard + "@storybook/addon-backgrounds@npm:6.5.10, @storybook/addon-backgrounds@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addon-backgrounds@npm:6.5.10" @@ -6793,6 +6935,35 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-backgrounds@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-backgrounds@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + global: ^4.4.0 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 67022905eb4c7104a28ae4feb3db99aa6f07b1f655fbeeb6bff6897dc2f70e452f7f799a98430c07242b2ba1b7bb0104a1879d9bf8fa855fea8293b6109f3a59 + languageName: node + linkType: hard + "@storybook/addon-controls@npm:6.5.10": version: 6.5.10 resolution: "@storybook/addon-controls@npm:6.5.10" @@ -6821,6 +6992,34 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-controls@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-controls@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/node-logger": 6.5.12 + "@storybook/store": 6.5.12 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + lodash: ^4.17.21 + ts-dedent: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 27ee396ae4ab411b1bd99eacb0ebe747aa36300dc3b787d48eb685a446e64e33ff7df3b8713943132b6dbe3c78af3d2d6115b4da4c6196ec351d843690ab8a55 + languageName: node + linkType: hard + "@storybook/addon-docs@npm:6.5.10, @storybook/addon-docs@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addon-docs@npm:6.5.10" @@ -6868,6 +7067,53 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-docs@npm:6.5.12, @storybook/addon-docs@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-docs@npm:6.5.12" + dependencies: + "@babel/plugin-transform-react-jsx": ^7.12.12 + "@babel/preset-env": ^7.12.11 + "@jest/transform": ^26.6.2 + "@mdx-js/react": ^1.6.22 + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/docs-tools": 6.5.12 + "@storybook/mdx1-csf": ^0.0.1 + "@storybook/node-logger": 6.5.12 + "@storybook/postinstall": 6.5.12 + "@storybook/preview-web": 6.5.12 + "@storybook/source-loader": 6.5.12 + "@storybook/store": 6.5.12 + "@storybook/theming": 6.5.12 + babel-loader: ^8.0.0 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + regenerator-runtime: ^0.13.7 + remark-external-links: ^8.0.0 + remark-slug: ^6.0.0 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + "@storybook/mdx2-csf": ^0.0.3 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@storybook/mdx2-csf": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 671e6cf220a3657cb275dadfee6e5a3ea165c939b9919dac984881795a8d369bb01a45ab368f371d9499dd65743392969a46527d858c780de13871b9df8cdcdf + languageName: node + linkType: hard + "@storybook/addon-essentials@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addon-essentials@npm:6.5.10" @@ -6928,6 +7174,66 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-essentials@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-essentials@npm:6.5.12" + dependencies: + "@storybook/addon-actions": 6.5.12 + "@storybook/addon-backgrounds": 6.5.12 + "@storybook/addon-controls": 6.5.12 + "@storybook/addon-docs": 6.5.12 + "@storybook/addon-measure": 6.5.12 + "@storybook/addon-outline": 6.5.12 + "@storybook/addon-toolbars": 6.5.12 + "@storybook/addon-viewport": 6.5.12 + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/node-logger": 6.5.12 + core-js: ^3.8.2 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + "@babel/core": ^7.9.6 + peerDependenciesMeta: + "@storybook/angular": + optional: true + "@storybook/builder-manager4": + optional: true + "@storybook/builder-manager5": + optional: true + "@storybook/builder-webpack4": + optional: true + "@storybook/builder-webpack5": + optional: true + "@storybook/html": + optional: true + "@storybook/vue": + optional: true + "@storybook/vue3": + optional: true + "@storybook/web-components": + optional: true + lit: + optional: true + lit-html: + optional: true + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + sveltedoc-parser: + optional: true + vue: + optional: true + webpack: + optional: true + checksum: f69d545f7fac829d7688f132bc2a8e59fc387992864f7edd26ec52b993223f29a42d8805c75b5074a3c4fb2061b661b18d5d0971d3010982996e451407f74fea + languageName: node + linkType: hard + "@storybook/addon-interactions@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addon-interactions@npm:6.5.10" @@ -7043,6 +7349,30 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-measure@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-measure@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + global: ^4.4.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: d47015e4e48f9d172d3faae9a49c1b3c749989299d6743ca29c646c4b2d43505c66678c77c035f524b50b54a520ae701def4da81daf80e2649b67174e05df89c + languageName: node + linkType: hard + "@storybook/addon-outline@npm:6.5.10": version: 6.5.10 resolution: "@storybook/addon-outline@npm:6.5.10" @@ -7069,6 +7399,32 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-outline@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-outline@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 3ba7859e27178450107cae7210aa9b41ce20d9b050ad5281ef25ff9b513f132c3a0bd460d69840d7d177c7c32aa6abe71875514f373569c86f64ad24feca0f15 + languageName: node + linkType: hard + "@storybook/addon-postcss@npm:~2.0.0": version: 2.0.0 resolution: "@storybook/addon-postcss@npm:2.0.0" @@ -7105,6 +7461,29 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-toolbars@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-toolbars@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 0b75ec7c9f7a789fb8cf2de0c2a8cf9a72c7f241be1bbe0b1faf832146bab29c722f825b64c1093b6e6127c7316d8a8f0aeeada4249915063d4795a7e79c3b0a + languageName: node + linkType: hard + "@storybook/addon-viewport@npm:6.5.10, @storybook/addon-viewport@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addon-viewport@npm:6.5.10" @@ -7132,7 +7511,34 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.10, @storybook/addons@npm:~6.5.10": +"@storybook/addon-viewport@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addon-viewport@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + global: ^4.4.0 + memoizerific: ^1.11.3 + prop-types: ^15.7.2 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 72d4b8a1b22456d3fa1cbe18c4804e3bae17790078a356e8ff43b2262842253f019b2cddbeeffbaef19fc06eecb55e48d895b2c9118238fb103d37aff8790378 + languageName: node + linkType: hard + +"@storybook/addons@npm:6.5.10, @storybook/addons@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/addons@npm:6.5.10" dependencies: @@ -7154,6 +7560,28 @@ __metadata: languageName: node linkType: hard +"@storybook/addons@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/addons@npm:6.5.12" + dependencies: + "@storybook/api": 6.5.12 + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.12 + "@storybook/theming": 6.5.12 + "@types/webpack-env": ^1.16.0 + core-js: ^3.8.2 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: c6242a80c7355544eb309603e77fdc3787d78ad983aba931f00812aeba75cc2cbd0e98c1ac0ce01441b58fabcdb671a9a799358f4bd6511cab289bc030d91f61 + languageName: node + linkType: hard + "@storybook/addons@npm:6.5.9": version: 6.5.9 resolution: "@storybook/addons@npm:6.5.9" @@ -7204,6 +7632,34 @@ __metadata: languageName: node linkType: hard +"@storybook/api@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/api@npm:6.5.12" + dependencies: + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.12 + "@storybook/semver": ^7.3.2 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + store2: ^2.12.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 3982cea5aaf851ccc19ff97ef82b7590d47839f0ebee28399e3b9381578edc130b4e46fe36431c62fa281949b2e0d5da2b1feafab0c2d24f70c4097d800b2679 + languageName: node + linkType: hard + "@storybook/api@npm:6.5.9": version: 6.5.9 resolution: "@storybook/api@npm:6.5.9" @@ -7293,6 +7749,67 @@ __metadata: languageName: node linkType: hard +"@storybook/builder-webpack4@npm:6.5.12, @storybook/builder-webpack4@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/builder-webpack4@npm:6.5.12" + dependencies: + "@babel/core": ^7.12.10 + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/channel-postmessage": 6.5.12 + "@storybook/channels": 6.5.12 + "@storybook/client-api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/node-logger": 6.5.12 + "@storybook/preview-web": 6.5.12 + "@storybook/router": 6.5.12 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.12 + "@storybook/theming": 6.5.12 + "@storybook/ui": 6.5.12 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/webpack": ^4.41.26 + autoprefixer: ^9.8.6 + babel-loader: ^8.0.0 + case-sensitive-paths-webpack-plugin: ^2.3.0 + core-js: ^3.8.2 + css-loader: ^3.6.0 + file-loader: ^6.2.0 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^4.1.6 + glob: ^7.1.6 + glob-promise: ^3.4.0 + global: ^4.4.0 + html-webpack-plugin: ^4.0.0 + pnp-webpack-plugin: 1.6.4 + postcss: ^7.0.36 + postcss-flexbugs-fixes: ^4.2.1 + postcss-loader: ^4.2.0 + raw-loader: ^4.0.2 + stable: ^0.1.8 + style-loader: ^1.3.0 + terser-webpack-plugin: ^4.2.3 + ts-dedent: ^2.0.0 + url-loader: ^4.1.1 + util-deprecate: ^1.0.2 + webpack: 4 + webpack-dev-middleware: ^3.7.3 + webpack-filter-warnings-plugin: ^1.2.1 + webpack-hot-middleware: ^2.25.1 + webpack-virtual-modules: ^0.2.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 3cb72ade60fc0767480c424cd5da6659027c35759852198936530c555d7fa1ae18326b3d696def06a54a55f7f26a6eb0246e675ea566c5bbfc63db54e0ad8880 + languageName: node + linkType: hard + "@storybook/builder-webpack5@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/builder-webpack5@npm:6.5.10" @@ -7360,6 +7877,21 @@ __metadata: languageName: node linkType: hard +"@storybook/channel-postmessage@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/channel-postmessage@npm:6.5.12" + dependencies: + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + core-js: ^3.8.2 + global: ^4.4.0 + qs: ^6.10.0 + telejson: ^6.0.8 + checksum: c225f848f4774e8159b9fd8bd904520ab2755f46ac6ef5a8ed7193b5cd79856e0bb797d10adfa0a1db9b9df075c41b4487d195cc451fb65b04737db70e5db6db + languageName: node + linkType: hard + "@storybook/channel-websocket@npm:6.5.10": version: 6.5.10 resolution: "@storybook/channel-websocket@npm:6.5.10" @@ -7373,6 +7905,19 @@ __metadata: languageName: node linkType: hard +"@storybook/channel-websocket@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/channel-websocket@npm:6.5.12" + dependencies: + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + core-js: ^3.8.2 + global: ^4.4.0 + telejson: ^6.0.8 + checksum: 03d4ed3f2b67daceea06325c8705b95013b65d014dfda769a923b2c1c890d1eca1fe8eea09e3386cc572c738ce43bc8951225ebd92de4205ad80438bb9e36ebe + languageName: node + linkType: hard + "@storybook/channels@npm:6.5.10": version: 6.5.10 resolution: "@storybook/channels@npm:6.5.10" @@ -7384,6 +7929,17 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/channels@npm:6.5.12" + dependencies: + core-js: ^3.8.2 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: e6b240a6c62a68a485bf8f4db536df0504cfcbe9685654e5a5712b833917b9a620e91994bf2283a420e413511c967e92ead522c98ad7e6c0e88b3830ddfd4e30 + languageName: node + linkType: hard + "@storybook/channels@npm:6.5.9": version: 6.5.9 resolution: "@storybook/channels@npm:6.5.9" @@ -7426,6 +7982,37 @@ __metadata: languageName: node linkType: hard +"@storybook/client-api@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/client-api@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/channel-postmessage": 6.5.12 + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.12 + "@types/qs": ^6.9.5 + "@types/webpack-env": ^1.16.0 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + store2: ^2.12.0 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 6a103cdf1c0499e238e6a652f192b3287b7bce2a96c194c48854d1e6d8e470568aa36307aa49650e8322965295b10d601c4a087e2cb7e3515bb9ba8281aeda35 + languageName: node + linkType: hard + "@storybook/client-logger@npm:6.5.10": version: 6.5.10 resolution: "@storybook/client-logger@npm:6.5.10" @@ -7436,6 +8023,16 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/client-logger@npm:6.5.12" + dependencies: + core-js: ^3.8.2 + global: ^4.4.0 + checksum: bd11bc25115f9b4a965e378d7dac28f9152038173ab5debb1e116a7aba69c814752d2c8aa4092dd1fc3f60cd99d4896c9e74d5e6f3c85768e7633adaf5bd2bf2 + languageName: node + linkType: hard + "@storybook/client-logger@npm:6.5.9, @storybook/client-logger@npm:^6.4.0": version: 6.5.9 resolution: "@storybook/client-logger@npm:6.5.9" @@ -7465,6 +8062,25 @@ __metadata: languageName: node linkType: hard +"@storybook/components@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/components@npm:6.5.12" + dependencies: + "@storybook/client-logger": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: fa469ae615d9146df7e23f01b85731d27e6400e2d94035db172deb1f61903d86c121d858558dd12307ecc6344d21b496db020731e73eff6ace3f82672b953a93 + languageName: node + linkType: hard + "@storybook/core-client@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core-client@npm:6.5.10" @@ -7500,6 +8116,41 @@ __metadata: languageName: node linkType: hard +"@storybook/core-client@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/core-client@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/channel-postmessage": 6.5.12 + "@storybook/channel-websocket": 6.5.12 + "@storybook/client-api": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/preview-web": 6.5.12 + "@storybook/store": 6.5.12 + "@storybook/ui": 6.5.12 + airbnb-js-shims: ^2.2.1 + ansi-to-html: ^0.6.11 + core-js: ^3.8.2 + global: ^4.4.0 + lodash: ^4.17.21 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + unfetch: ^4.2.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 4fb567964a6c15526ee6ee882e20d72c650ad0c74504ee2a058c13856efd25a0c9c7f666d36c6b4e70a75ece73c1ed812f554bbbf87771cd6171cd09ebf31410 + languageName: node + linkType: hard + "@storybook/core-common@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core-common@npm:6.5.10" @@ -7564,6 +8215,70 @@ __metadata: languageName: node linkType: hard +"@storybook/core-common@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/core-common@npm:6.5.12" + dependencies: + "@babel/core": ^7.12.10 + "@babel/plugin-proposal-class-properties": ^7.12.1 + "@babel/plugin-proposal-decorators": ^7.12.12 + "@babel/plugin-proposal-export-default-from": ^7.12.1 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.12.1 + "@babel/plugin-proposal-object-rest-spread": ^7.12.1 + "@babel/plugin-proposal-optional-chaining": ^7.12.7 + "@babel/plugin-proposal-private-methods": ^7.12.1 + "@babel/plugin-proposal-private-property-in-object": ^7.12.1 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-transform-arrow-functions": ^7.12.1 + "@babel/plugin-transform-block-scoping": ^7.12.12 + "@babel/plugin-transform-classes": ^7.12.1 + "@babel/plugin-transform-destructuring": ^7.12.1 + "@babel/plugin-transform-for-of": ^7.12.1 + "@babel/plugin-transform-parameters": ^7.12.1 + "@babel/plugin-transform-shorthand-properties": ^7.12.1 + "@babel/plugin-transform-spread": ^7.12.1 + "@babel/preset-env": ^7.12.11 + "@babel/preset-react": ^7.12.10 + "@babel/preset-typescript": ^7.12.7 + "@babel/register": ^7.12.1 + "@storybook/node-logger": 6.5.12 + "@storybook/semver": ^7.3.2 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/pretty-hrtime": ^1.0.0 + babel-loader: ^8.0.0 + babel-plugin-macros: ^3.0.1 + babel-plugin-polyfill-corejs3: ^0.1.0 + chalk: ^4.1.0 + core-js: ^3.8.2 + express: ^4.17.1 + file-system-cache: ^1.0.5 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^6.0.4 + fs-extra: ^9.0.1 + glob: ^7.1.6 + handlebars: ^4.7.7 + interpret: ^2.2.0 + json5: ^2.1.3 + lazy-universal-dotenv: ^3.0.1 + picomatch: ^2.3.0 + pkg-dir: ^5.0.0 + pretty-hrtime: ^1.0.3 + resolve-from: ^5.0.0 + slash: ^3.0.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + webpack: 4 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: d12b276718d3bb527084135882abc35fcdc4690896579b9f5e0417236a0d02f2791424ac62a891a0353af635be10c22db7dfe04d0db644ab0757ae3981f0ff1a + languageName: node + linkType: hard + "@storybook/core-events@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core-events@npm:6.5.10" @@ -7573,6 +8288,15 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/core-events@npm:6.5.12" + dependencies: + core-js: ^3.8.2 + checksum: 82a4b9cb2a8599f3916db84b08b4cfbde8f56cb96a7afe641b3f144676fc7dc5a705e65f8844430b36ee6e6e14d6b2cb741622a8b39411276682219df5e04271 + languageName: node + linkType: hard + "@storybook/core-events@npm:6.5.9": version: 6.5.9 resolution: "@storybook/core-events@npm:6.5.9" @@ -7645,6 +8369,69 @@ __metadata: languageName: node linkType: hard +"@storybook/core-server@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/core-server@npm:6.5.12" + dependencies: + "@discoveryjs/json-ext": ^0.5.3 + "@storybook/builder-webpack4": 6.5.12 + "@storybook/core-client": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/csf-tools": 6.5.12 + "@storybook/manager-webpack4": 6.5.12 + "@storybook/node-logger": 6.5.12 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.12 + "@storybook/telemetry": 6.5.12 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/node-fetch": ^2.5.7 + "@types/pretty-hrtime": ^1.0.0 + "@types/webpack": ^4.41.26 + better-opn: ^2.1.1 + boxen: ^5.1.2 + chalk: ^4.1.0 + cli-table3: ^0.6.1 + commander: ^6.2.1 + compression: ^1.7.4 + core-js: ^3.8.2 + cpy: ^8.1.2 + detect-port: ^1.3.0 + express: ^4.17.1 + fs-extra: ^9.0.1 + global: ^4.4.0 + globby: ^11.0.2 + ip: ^2.0.0 + lodash: ^4.17.21 + node-fetch: ^2.6.7 + open: ^8.4.0 + pretty-hrtime: ^1.0.3 + prompts: ^2.4.0 + regenerator-runtime: ^0.13.7 + serve-favicon: ^2.5.0 + slash: ^3.0.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + watchpack: ^2.2.0 + webpack: 4 + ws: ^8.2.3 + x-default-browser: ^0.4.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + checksum: 1e7e8de948012eb126f30261d23a552e18e671ad7796c46051e17be545c37a70c72437cbb24249d2e88b3525ed6a4b7205ddeeb167aca6b9478df85b128a57d3 + languageName: node + linkType: hard + "@storybook/core@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core@npm:6.5.10" @@ -7666,6 +8453,27 @@ __metadata: languageName: node linkType: hard +"@storybook/core@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/core@npm:6.5.12" + dependencies: + "@storybook/core-client": 6.5.12 + "@storybook/core-server": 6.5.12 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: "*" + peerDependenciesMeta: + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + checksum: 82606be8f89ad34a662d366e64d85af5a61270e2cff4dab8c3c3b0ec17ae3e34e560b54a1e2dd3fdd52eb072f8101b409e762f8581e65abcf07097e768baaaf0 + languageName: node + linkType: hard + "@storybook/csf-tools@npm:6.5.10": version: 6.5.10 resolution: "@storybook/csf-tools@npm:6.5.10" @@ -7693,6 +8501,33 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-tools@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/csf-tools@npm:6.5.12" + dependencies: + "@babel/core": ^7.12.10 + "@babel/generator": ^7.12.11 + "@babel/parser": ^7.12.11 + "@babel/plugin-transform-react-jsx": ^7.12.12 + "@babel/preset-env": ^7.12.11 + "@babel/traverse": ^7.12.11 + "@babel/types": ^7.12.11 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/mdx1-csf": ^0.0.1 + core-js: ^3.8.2 + fs-extra: ^9.0.1 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + "@storybook/mdx2-csf": ^0.0.3 + peerDependenciesMeta: + "@storybook/mdx2-csf": + optional: true + checksum: 21da554c88f22ee583cd1956cf440506212d9e8727c7f0a493a92804e58b83d3fcfa18d3081a9fd1b5e8da07a1cfbee15bfa638a13a8fad585eac04fe26f5112 + languageName: node + linkType: hard + "@storybook/csf@npm:0.0.2--canary.4566f4d.1": version: 0.0.2--canary.4566f4d.1 resolution: "@storybook/csf@npm:0.0.2--canary.4566f4d.1" @@ -7702,6 +8537,15 @@ __metadata: languageName: node linkType: hard +"@storybook/csf@npm:^0.0.1": + version: 0.0.1 + resolution: "@storybook/csf@npm:0.0.1" + dependencies: + lodash: ^4.17.15 + checksum: fb57fa028b08a51edf44e1a2bf4be40a4607f5c6ccb58aae8924f476a42b9bbd61a0ad521cfc82196f23e6a912caae0a615e70a755e6800b284c91c509fd2de6 + languageName: node + linkType: hard + "@storybook/docs-tools@npm:6.5.10": version: 6.5.10 resolution: "@storybook/docs-tools@npm:6.5.10" @@ -7717,6 +8561,21 @@ __metadata: languageName: node linkType: hard +"@storybook/docs-tools@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/docs-tools@npm:6.5.12" + dependencies: + "@babel/core": ^7.12.10 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.12 + core-js: ^3.8.2 + doctrine: ^3.0.0 + lodash: ^4.17.21 + regenerator-runtime: ^0.13.7 + checksum: 9433b0bc74e739f37d4be857e366d74e56566cdf27f72f462eb09a6e84713aadc8c768e075fbe7a062be104bb1536c14b8dea1e890917ca94b1580e71dbe60be + languageName: node + linkType: hard + "@storybook/instrumenter@npm:6.5.10": version: 6.5.10 resolution: "@storybook/instrumenter@npm:6.5.10" @@ -7792,6 +8651,55 @@ __metadata: languageName: node linkType: hard +"@storybook/manager-webpack4@npm:6.5.12, @storybook/manager-webpack4@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/manager-webpack4@npm:6.5.12" + dependencies: + "@babel/core": ^7.12.10 + "@babel/plugin-transform-template-literals": ^7.12.1 + "@babel/preset-react": ^7.12.10 + "@storybook/addons": 6.5.12 + "@storybook/core-client": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/node-logger": 6.5.12 + "@storybook/theming": 6.5.12 + "@storybook/ui": 6.5.12 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/webpack": ^4.41.26 + babel-loader: ^8.0.0 + case-sensitive-paths-webpack-plugin: ^2.3.0 + chalk: ^4.1.0 + core-js: ^3.8.2 + css-loader: ^3.6.0 + express: ^4.17.1 + file-loader: ^6.2.0 + find-up: ^5.0.0 + fs-extra: ^9.0.1 + html-webpack-plugin: ^4.0.0 + node-fetch: ^2.6.7 + pnp-webpack-plugin: 1.6.4 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + resolve-from: ^5.0.0 + style-loader: ^1.3.0 + telejson: ^6.0.8 + terser-webpack-plugin: ^4.2.3 + ts-dedent: ^2.0.0 + url-loader: ^4.1.1 + util-deprecate: ^1.0.2 + webpack: 4 + webpack-dev-middleware: ^3.7.3 + webpack-virtual-modules: ^0.2.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 89c6ab508a930def13403275201e1e7efd667a25f0e04a6a4fe83e83d8a77065309f11c6ba183bb412072b80580e232365789d2317ae9c1e92df92c4061752be + languageName: node + linkType: hard + "@storybook/manager-webpack5@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/manager-webpack5@npm:6.5.10" @@ -7870,6 +8778,19 @@ __metadata: languageName: node linkType: hard +"@storybook/node-logger@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/node-logger@npm:6.5.12" + dependencies: + "@types/npmlog": ^4.1.2 + chalk: ^4.1.0 + core-js: ^3.8.2 + npmlog: ^5.0.1 + pretty-hrtime: ^1.0.3 + checksum: 7589477486a25e67d9119e9c363e8bde23e52601043a506ac0d28f4d353f3a228face79b40a2eb0cc0c7c8b05ed084336fef5dcc3213ed4484527c6631eafeb0 + languageName: node + linkType: hard + "@storybook/node-logger@npm:^6.1.14": version: 6.5.9 resolution: "@storybook/node-logger@npm:6.5.9" @@ -7888,7 +8809,16 @@ __metadata: resolution: "@storybook/postinstall@npm:6.5.10" dependencies: core-js: ^3.8.2 - checksum: ee6355953cb0d4c49392f59502465f967846253afed6df24c845d028e51c0a4b19dadc092a837b29ba8c7fea6529b1ca757b86e579deac7fc3decac2dd8d0247 + checksum: ee6355953cb0d4c49392f59502465f967846253afed6df24c845d028e51c0a4b19dadc092a837b29ba8c7fea6529b1ca757b86e579deac7fc3decac2dd8d0247 + languageName: node + linkType: hard + +"@storybook/postinstall@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/postinstall@npm:6.5.12" + dependencies: + core-js: ^3.8.2 + checksum: 0f84be944501d20fb12b554fe46967182fe1824acee40dbcead848c1f29ab3433980ef1cb0cc1c7dffdac7145459f3b38d37b8047d07aa83bee4909aeb27f23a languageName: node linkType: hard @@ -7919,6 +8849,33 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-web@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/preview-web@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/channel-postmessage": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.12 + ansi-to-html: ^0.6.11 + core-js: ^3.8.2 + global: ^4.4.0 + lodash: ^4.17.21 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + unfetch: ^4.2.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: e11671fd136042a0ac19be6749f40bcdf858adb6fbc36998eb2c1737fc622c15df233eb04ff3cf555a61a31d32e5218cf9ff3ff9b941b457a8159a8ca54ab2e8 + languageName: node + linkType: hard + "@storybook/react-docgen-typescript-plugin@npm:1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": version: 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 resolution: "@storybook/react-docgen-typescript-plugin@npm:1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0" @@ -8002,6 +8959,71 @@ __metadata: languageName: node linkType: hard +"@storybook/react@npm:~6.5.12": + version: 6.5.12 + resolution: "@storybook/react@npm:6.5.12" + dependencies: + "@babel/preset-flow": ^7.12.1 + "@babel/preset-react": ^7.12.10 + "@pmmmwh/react-refresh-webpack-plugin": ^0.5.3 + "@storybook/addons": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core": 6.5.12 + "@storybook/core-common": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/docs-tools": 6.5.12 + "@storybook/node-logger": 6.5.12 + "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.12 + "@types/estree": ^0.0.51 + "@types/node": ^14.14.20 || ^16.0.0 + "@types/webpack-env": ^1.16.0 + acorn: ^7.4.1 + acorn-jsx: ^5.3.1 + acorn-walk: ^7.2.0 + babel-plugin-add-react-displayname: ^0.0.5 + babel-plugin-react-docgen: ^4.2.1 + core-js: ^3.8.2 + escodegen: ^2.0.0 + fs-extra: ^9.0.1 + global: ^4.4.0 + html-tags: ^3.1.0 + lodash: ^4.17.21 + prop-types: ^15.7.2 + react-element-to-jsx-string: ^14.3.4 + react-refresh: ^0.11.0 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + webpack: ">=4.43.0 <6.0.0" + peerDependencies: + "@babel/core": ^7.11.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + require-from-string: ^2.0.2 + peerDependenciesMeta: + "@babel/core": + optional: true + "@storybook/builder-webpack4": + optional: true + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack4": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + bin: + build-storybook: bin/build.js + start-storybook: bin/index.js + storybook-server: bin/index.js + checksum: 7b762f5b0db2b94d3e492ed45a7566009dc9ff008c9b29db278d6404e1c7c9f419a0114090bf23fc5b3e193a83d74e21f0fe6ba04105eb30f59be1d4c87d652d + languageName: node + linkType: hard + "@storybook/router@npm:6.5.10": version: 6.5.10 resolution: "@storybook/router@npm:6.5.10" @@ -8018,6 +9040,22 @@ __metadata: languageName: node linkType: hard +"@storybook/router@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/router@npm:6.5.12" + dependencies: + "@storybook/client-logger": 6.5.12 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 545f4b767021b88f82eac69b9356fa5fa3a5866285c3a34fa762abc5743e3280895858aa5c820195d95a04c6768299191d4dd788a7d9fd3f17ff1d8236c0ba75 + languageName: node + linkType: hard + "@storybook/router@npm:6.5.9": version: 6.5.9 resolution: "@storybook/router@npm:6.5.9" @@ -8067,6 +9105,27 @@ __metadata: languageName: node linkType: hard +"@storybook/source-loader@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/source-loader@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + estraverse: ^5.2.0 + global: ^4.4.0 + loader-utils: ^2.0.0 + lodash: ^4.17.21 + prettier: ">=2.2.1 <=2.3.0" + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: ad6b0774877678d8495e7afaeb820adbc2d1b091df5678da39123eca875cc30fe41f013c2db0a4c5f7614529dc46be00cb165484e65a960d3fe7657b04cf418f + languageName: node + linkType: hard + "@storybook/store@npm:6.5.10": version: 6.5.10 resolution: "@storybook/store@npm:6.5.10" @@ -8093,6 +9152,32 @@ __metadata: languageName: node linkType: hard +"@storybook/store@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/store@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + slash: ^3.0.0 + stable: ^0.1.8 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 7fab43471c692cda33e9cbb7abf8a932bf922bd01bf26c56a3dd909f57ff0f26a80c6456a41c5be01191bb818536f16f5c98617d263b49a60e432b0acca07949 + languageName: node + linkType: hard + "@storybook/telemetry@npm:6.5.10": version: 6.5.10 resolution: "@storybook/telemetry@npm:6.5.10" @@ -8113,6 +9198,26 @@ __metadata: languageName: node linkType: hard +"@storybook/telemetry@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/telemetry@npm:6.5.12" + dependencies: + "@storybook/client-logger": 6.5.12 + "@storybook/core-common": 6.5.12 + chalk: ^4.1.0 + core-js: ^3.8.2 + detect-package-manager: ^2.0.1 + fetch-retry: ^5.0.2 + fs-extra: ^9.0.1 + global: ^4.4.0 + isomorphic-unfetch: ^3.1.0 + nanoid: ^3.3.1 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + checksum: fe465e31e20bc271b1b066a1c1fc4ea8b7cdca1858bc875e19d7ad4e7988c0cff084ce822dc0657ecdefafa02a416dd239753c9c197f6758b39d9260964d631c + languageName: node + linkType: hard + "@storybook/testing-library@npm:0.0.13, @storybook/testing-library@npm:~0.0.13": version: 0.0.13 resolution: "@storybook/testing-library@npm:0.0.13" @@ -8141,6 +9246,21 @@ __metadata: languageName: node linkType: hard +"@storybook/theming@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/theming@npm:6.5.12" + dependencies: + "@storybook/client-logger": 6.5.12 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a982ebf88c7e1e21127febd17feebf26ac8d655f0c868bf110cbcaaef87eedb257300087618c525cb654808b590dc4b7b98dd6fec92fd76a040441d86c4b8289 + languageName: node + linkType: hard + "@storybook/theming@npm:6.5.9": version: 6.5.9 resolution: "@storybook/theming@npm:6.5.9" @@ -8181,6 +9301,31 @@ __metadata: languageName: node linkType: hard +"@storybook/ui@npm:6.5.12": + version: 6.5.12 + resolution: "@storybook/ui@npm:6.5.12" + dependencies: + "@storybook/addons": 6.5.12 + "@storybook/api": 6.5.12 + "@storybook/channels": 6.5.12 + "@storybook/client-logger": 6.5.12 + "@storybook/components": 6.5.12 + "@storybook/core-events": 6.5.12 + "@storybook/router": 6.5.12 + "@storybook/semver": ^7.3.2 + "@storybook/theming": 6.5.12 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + resolve-from: ^5.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 026ddc42d00773ad711824a5374b040b589e8a87b8500db4d6a8cb67263eba0789d6e6afe7fd8e0fcb798380eba210b4225af197f064d3e88ec0bda97a83b28b + languageName: node + linkType: hard + "@tanstack/query-core@npm:^4.0.0-beta.1": version: 4.0.10 resolution: "@tanstack/query-core@npm:4.0.10" @@ -9663,6 +10808,17 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/experimental-utils@npm:^5.3.0": + version: 5.36.2 + resolution: "@typescript-eslint/experimental-utils@npm:5.36.2" + dependencies: + "@typescript-eslint/utils": 5.36.2 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 2a40e47d11e084390a03a66d4802863c269517ad3fafd7105797039c1e0b5dc4d52653aad2c8511e862bc8017bb67bf3910cf3c5a1b1828d6df7f9086eba66c1 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^5.30.7": version: 5.30.7 resolution: "@typescript-eslint/parser@npm:5.30.7" @@ -9690,6 +10846,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.36.2": + version: 5.36.2 + resolution: "@typescript-eslint/scope-manager@npm:5.36.2" + dependencies: + "@typescript-eslint/types": 5.36.2 + "@typescript-eslint/visitor-keys": 5.36.2 + checksum: 93ff655f7c237c88ec6dc5911202dd8f81bd8909b27f1a758a9d77e9791040f1ee6fe2891314bde75c808ce586246e98003a1b1396937b0312f2440016dea751 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/type-utils@npm:5.30.7" @@ -9713,6 +10879,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.36.2": + version: 5.36.2 + resolution: "@typescript-eslint/types@npm:5.36.2" + checksum: 736cb8a76b58f2f9a7d066933094c5510ffe31479ea8b804a829ec85942420f1b55e0eb2688fbdaaaa9c0e5b3b590fb8f14bbd745353696b4fd33fda620d417b + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/typescript-estree@npm:5.30.7" @@ -9731,6 +10904,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.36.2": + version: 5.36.2 + resolution: "@typescript-eslint/typescript-estree@npm:5.36.2" + dependencies: + "@typescript-eslint/types": 5.36.2 + "@typescript-eslint/visitor-keys": 5.36.2 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 2827ff57a114b6107ea6d555f3855007133b08a7c2bafba0cfa0c935d8b99fd7b49e982d48cccc1c5ba550d95748d0239f5e2109893f12a165d76ed64a0d261b + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.30.7, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": version: 5.30.7 resolution: "@typescript-eslint/utils@npm:5.30.7" @@ -9747,6 +10938,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:5.36.2": + version: 5.36.2 + resolution: "@typescript-eslint/utils@npm:5.36.2" + dependencies: + "@types/json-schema": ^7.0.9 + "@typescript-eslint/scope-manager": 5.36.2 + "@typescript-eslint/types": 5.36.2 + "@typescript-eslint/typescript-estree": 5.36.2 + eslint-scope: ^5.1.1 + eslint-utils: ^3.0.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 45356cf55a8733e3ab1f2c3c19cdaefdb79857e35eb1433c29b81f3df071e9cef8a286bc407abe243889a21d9e793e999f92f03b9c727a0fac1c17a48e64c42a + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/visitor-keys@npm:5.30.7" @@ -9757,6 +10964,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.36.2": + version: 5.36.2 + resolution: "@typescript-eslint/visitor-keys@npm:5.36.2" + dependencies: + "@typescript-eslint/types": 5.36.2 + eslint-visitor-keys: ^3.3.0 + checksum: 87ccdcfa5cdedaa3a1aac30d656969f4f5910b62bcaacdf80a514dbf0cbbd8e79b55f8e987eab34cc79ece8ce4b8c19d5caf8b0afb74e0b0d7ab39fb29aa8eba + languageName: node + linkType: hard + "@ungap/promise-all-settled@npm:1.1.2": version: 1.1.2 resolution: "@ungap/promise-all-settled@npm:1.1.2" @@ -16382,6 +17599,20 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-storybook@npm:^0.6.4": + version: 0.6.4 + resolution: "eslint-plugin-storybook@npm:0.6.4" + dependencies: + "@storybook/csf": ^0.0.1 + "@typescript-eslint/experimental-utils": ^5.3.0 + requireindex: ^1.1.0 + ts-dedent: ^2.2.0 + peerDependencies: + eslint: ">=6" + checksum: 9bc1befb50ed741ee27f0492ba5c8c6c3d8d54a8ac53bd6c2054046a2918829d80b0f0ff916abe10c7e01fb45d29ec284ff9d19d0ef35d7812367b4f708756a3 + languageName: node + linkType: hard + "eslint-plugin-testing-library@npm:^5.5.1": version: 5.5.1 resolution: "eslint-plugin-testing-library@npm:5.5.1" @@ -16574,6 +17805,55 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.22.0": + version: 8.23.1 + resolution: "eslint@npm:8.23.1" + dependencies: + "@eslint/eslintrc": ^1.3.2 + "@humanwhocodes/config-array": ^0.10.4 + "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 + "@humanwhocodes/module-importer": ^1.0.1 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.1.1 + eslint-utils: ^3.0.0 + eslint-visitor-keys: ^3.3.0 + espree: ^9.4.0 + esquery: ^1.4.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.1 + globals: ^13.15.0 + globby: ^11.1.0 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + js-sdsl: ^4.1.4 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + regexpp: ^3.2.0 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: a727e15492786a03b438bcf021db49f715680679846a7b8d79b98ad34576f2a570404ffe882d3c3e26f6359bff7277ef11fae5614bfe8629adb653f20d018c71 + languageName: node + linkType: hard + "eslint@npm:~8.8.0": version: 8.8.0 resolution: "eslint@npm:8.8.0" @@ -16641,6 +17921,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.4.0": + version: 9.4.0 + resolution: "espree@npm:9.4.0" + dependencies: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.3.0 + checksum: 2e3020dde67892d2ba3632413b44d0dc31d92c29ce72267d7ec24216a562f0a6494d3696e2fa39a3ec8c0e0088d773947ab2925fbb716801a11eb8dd313ac89c + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -22496,6 +23787,13 @@ __metadata: languageName: node linkType: hard +"js-sdsl@npm:^4.1.4": + version: 4.1.4 + resolution: "js-sdsl@npm:4.1.4" + checksum: 1977cea4ab18e0e03e28bdf0371d8b443fad65ca0988e0faa216406faf6bb943714fe8f7cc7a5bfe5f35ba3d94ddae399f4d10200f547f2c3320688b0670d726 + languageName: node + linkType: hard + "js-sha256@npm:^0.9.0": version: 0.9.0 resolution: "js-sha256@npm:0.9.0" @@ -30178,6 +31476,13 @@ __metadata: languageName: node linkType: hard +"requireindex@npm:^1.1.0": + version: 1.2.0 + resolution: "requireindex@npm:1.2.0" + checksum: 50d8b10a1ff1fdf6aea7a1870bc7bd238b0fb1917d8d7ca17fd03afc38a65dcd7a8a4eddd031f89128b5f0065833d5c92c4fef67f2c04e8624057fe626c9cf94 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" -- GitLab From 2857441ece319633739c1debdd6fcdf916f98fb4 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty <debdut.chakraborty@rocket.chat> Date: Fri, 16 Sep 2022 06:23:30 +0530 Subject: [PATCH 043/107] [FIX] Check admin setting for whether to display roles or not (#26601) --- .../views/room/MessageList/providers/MessageListProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 774c30e8149..c42c5ec3ba8 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -37,7 +37,9 @@ export const MessageListProvider: FC<{ const katexParenthesisSyntaxEnabled = Boolean(useSetting('Katex_Parenthesis_Syntax')); const showColors = useSetting('HexColorPreview_Enabled') as boolean; - const showRoles = Boolean(!useUserPreference<boolean>('hideRoles') && !isMobile); + const displayRolesGlobal = Boolean(useSetting('UI_DisplayRoles')); + const hideRolesPreference = Boolean(!useUserPreference<boolean>('hideRoles') && !isMobile); + const showRoles = displayRolesGlobal && hideRolesPreference; const showUsername = Boolean(!useUserPreference<boolean>('hideUsernames') && !isMobile); const highlights = useUserPreference<string[]>('highlights'); -- GitLab From 40dd7bb7d206b64be11da8bc07d26618861a11ae Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Thu, 15 Sep 2022 23:25:24 -0300 Subject: [PATCH 044/107] Chore: Fix API tests retry (#26860) --- .github/workflows/build_and_test.yml | 67 ++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d434f09433f..954e211263c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -264,22 +264,38 @@ jobs: tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz - - name: Start containers - env: - MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' - MONGO_OPLOG_URL: 'mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true' + - name: Docker env vars + id: docker-env run: | - export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}" + echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}" # test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once) if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then - export RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine" - export RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" + RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine" + RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" else - export RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile" - export RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" + RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile" + RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" fi; + echo "RC_DOCKERFILE: ${RC_DOCKERFILE}" + echo "::set-output name=rc-dockerfile::${RC_DOCKERFILE}" + + echo "RC_DOCKER_TAG: ${RC_DOCKER_TAG}" + echo "::set-output name=rc-docker-tag::${RC_DOCKER_TAG}" + + - name: Start containers + env: + MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' + MONGO_OPLOG_URL: 'mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true' + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + run: | docker compose -f docker-compose-ci.yml up -d --build rocketchat sleep 10 @@ -299,17 +315,13 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Publish Docker images to GitHub Container Registry + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' run: | - export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - - # test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once) - if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then - export RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" - else - export RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" - fi; - docker compose -f docker-compose-ci.yml push rocketchat if [[ '${{ matrix.mongodb-version }}' = '4.4' ]]; then @@ -322,6 +334,11 @@ jobs: fi; - name: E2E Test API + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} run: | docker ps docker compose -f docker-compose-ci.yml logs rocketchat --tail=50 @@ -336,9 +353,9 @@ jobs: docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z") - docker compose -f ../../docker-compose-ci.yml start rocketchat + docker compose -f ../../docker-compose-ci.yml restart rocketchat until echo "$(docker compose -f ../../docker-compose-ci.yml logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do echo "Waiting Rocket.Chat to start up" @@ -364,13 +381,17 @@ jobs: npx playwright install --with-deps - name: E2E Test UI + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} run: | docker ps docker compose -f docker-compose-ci.yml logs rocketchat --tail=50 docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z") docker compose -f docker-compose-ci.yml restart rocketchat @@ -502,7 +523,7 @@ jobs: docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z") docker compose -f ../../docker-compose-ci.yml start @@ -535,7 +556,7 @@ jobs: docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z") docker compose -f docker-compose-ci.yml restart -- GitLab From f9676c6bd002c5f05fe672ba5003620533798eb0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 16 Sep 2022 10:23:38 -0300 Subject: [PATCH 045/107] Chore: merge all functions using autorun x useSubscription pattern (#26886) --- apps/meteor/client/hooks/useReactiveValue.ts | 36 ++----------------- apps/meteor/client/hooks/useReactiveVar.ts | 24 +++---------- .../createReactiveSubscriptionFactory.ts | 22 ++++++------ apps/meteor/client/lib/createSubscription.ts | 7 ++++ .../client/lib/portals/createLazyElement.ts | 17 +++------ .../providers/AuthorizationProvider.tsx | 2 +- .../client/providers/RouterProvider.tsx | 18 +--------- .../client/providers/SessionProvider.tsx | 2 +- .../client/providers/SettingsProvider.tsx | 2 +- apps/meteor/client/providers/UserProvider.tsx | 2 +- .../settings/EditableSettingsProvider.tsx | 2 +- 11 files changed, 35 insertions(+), 99 deletions(-) rename apps/meteor/client/{providers => lib}/createReactiveSubscriptionFactory.ts (72%) create mode 100644 apps/meteor/client/lib/createSubscription.ts diff --git a/apps/meteor/client/hooks/useReactiveValue.ts b/apps/meteor/client/hooks/useReactiveValue.ts index 90d9919691d..49264af058a 100644 --- a/apps/meteor/client/hooks/useReactiveValue.ts +++ b/apps/meteor/client/hooks/useReactiveValue.ts @@ -1,40 +1,10 @@ -import { Tracker } from 'meteor/tracker'; import { useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -export const useReactiveValue = <T>(computeCurrentValue: () => T): T => { - const [subscribe, getSnapshot] = useMemo(() => { - const callbacks = new Set<() => void>(); - - let currentValue: T; - - const computation = Tracker.autorun(() => { - currentValue = computeCurrentValue(); - callbacks.forEach((callback) => { - callback(); - }); - }); - - const { stop } = computation; +import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; - computation.stop = (): void => undefined; - - return [ - (callback: () => void): (() => void) => { - callbacks.add(callback); - - return (): void => { - callbacks.delete(callback); - - if (callbacks.size === 0) { - computation.stop = stop; - computation.stop(); - } - }; - }, - (): T => currentValue, - ]; - }, [computeCurrentValue]); +export const useReactiveValue = <T>(computeCurrentValue: () => T): T => { + const [subscribe, getSnapshot] = useMemo(() => createReactiveSubscriptionFactory<T>(computeCurrentValue)(), [computeCurrentValue]); return useSyncExternalStore(subscribe, getSnapshot); }; diff --git a/apps/meteor/client/hooks/useReactiveVar.ts b/apps/meteor/client/hooks/useReactiveVar.ts index 48b6bb63d1a..d31581ca254 100644 --- a/apps/meteor/client/hooks/useReactiveVar.ts +++ b/apps/meteor/client/hooks/useReactiveVar.ts @@ -1,23 +1,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { useEffect, useState } from 'react'; +import { useCallback } from 'react'; -/** @deprecated */ -export const useReactiveVar = <T>(variable: ReactiveVar<T>): T => { - const [value, setValue] = useState(() => Tracker.nonreactive(() => variable.get())); - - useEffect(() => { - const computation = Tracker.nonreactive(() => - Tracker.autorun(() => { - const value = variable.get(); - setValue(() => value); - }), - ); +import { useReactiveValue } from './useReactiveValue'; - return (): void => { - computation?.stop(); - }; - }, [variable]); - - return value; -}; +/** @deprecated */ +export const useReactiveVar = <T>(variable: ReactiveVar<T>): T => useReactiveValue(useCallback(() => variable.get(), [variable])); diff --git a/apps/meteor/client/providers/createReactiveSubscriptionFactory.ts b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts similarity index 72% rename from apps/meteor/client/providers/createReactiveSubscriptionFactory.ts rename to apps/meteor/client/lib/createReactiveSubscriptionFactory.ts index 4dead8e2b29..b8e92de99d0 100644 --- a/apps/meteor/client/providers/createReactiveSubscriptionFactory.ts +++ b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts @@ -13,27 +13,27 @@ export const createReactiveSubscriptionFactory = let currentValue = computeCurrentValue(); - let computation: Tracker.Computation | undefined; - const timeout = setTimeout(() => { - computation = Tracker.autorun(() => { - currentValue = computeCurrentValue(); - callbacks.forEach((callback) => { - callback(); - }); + const computation = Tracker.autorun(() => { + currentValue = computeCurrentValue(); + callbacks.forEach((callback) => { + Tracker.afterFlush(callback); }); - }, 0); + }); + + const { stop } = computation; + + computation.stop = (): void => undefined; return [ (callback): (() => void) => { callbacks.add(callback); return (): void => { - clearTimeout(timeout); - callbacks.delete(callback); if (callbacks.size === 0) { - computation?.stop(); + computation.stop = stop; + computation.stop(); } }; }, diff --git a/apps/meteor/client/lib/createSubscription.ts b/apps/meteor/client/lib/createSubscription.ts new file mode 100644 index 00000000000..14ede16bc91 --- /dev/null +++ b/apps/meteor/client/lib/createSubscription.ts @@ -0,0 +1,7 @@ +import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; + +export const createSubscription = function <T>( + getValue: () => T, +): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T] { + return createReactiveSubscriptionFactory(getValue)(); +}; diff --git a/apps/meteor/client/lib/portals/createLazyElement.ts b/apps/meteor/client/lib/portals/createLazyElement.ts index 6a3ca6ea3c6..f5e42c0d014 100644 --- a/apps/meteor/client/lib/portals/createLazyElement.ts +++ b/apps/meteor/client/lib/portals/createLazyElement.ts @@ -1,5 +1,6 @@ -import { Tracker } from 'meteor/tracker'; -import { ComponentType, ReactElement, PropsWithoutRef, createElement, lazy, useEffect, useState, Suspense, FC } from 'react'; +import { ComponentType, ReactElement, PropsWithoutRef, createElement, lazy, Suspense, FC } from 'react'; + +import { useReactiveValue } from '../../hooks/useReactiveValue'; export const createLazyElement = <Props>( factory: () => Promise<{ default: ComponentType<Props> }>, @@ -12,17 +13,7 @@ export const createLazyElement = <Props>( } const WrappedComponent: FC = () => { - const [props, setProps] = useState(() => Tracker.nonreactive(getProps)); - - useEffect(() => { - const computation = Tracker.autorun(() => { - setProps(getProps()); - }); - - return (): void => { - computation.stop(); - }; - }, []); + const props = useReactiveValue(getProps); return createElement(Suspense, { fallback: null }, createElement(LazyComponent, props)); }; diff --git a/apps/meteor/client/providers/AuthorizationProvider.tsx b/apps/meteor/client/providers/AuthorizationProvider.tsx index c5e360169b6..e0c7a870775 100644 --- a/apps/meteor/client/providers/AuthorizationProvider.tsx +++ b/apps/meteor/client/providers/AuthorizationProvider.tsx @@ -7,7 +7,7 @@ import React, { FC, useCallback, useEffect } from 'react'; import { hasPermission, hasAtLeastOnePermission, hasAllPermission, hasRole } from '../../app/authorization/client'; import { Roles } from '../../app/models/client/models/Roles'; import { useReactiveValue } from '../hooks/useReactiveValue'; -import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; +import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; class RoleStore extends Emitter<{ change: { [_id: string]: IRole }; diff --git a/apps/meteor/client/providers/RouterProvider.tsx b/apps/meteor/client/providers/RouterProvider.tsx index 8a505723df3..45c3b41e9fc 100644 --- a/apps/meteor/client/providers/RouterProvider.tsx +++ b/apps/meteor/client/providers/RouterProvider.tsx @@ -1,24 +1,8 @@ import { RouterContext, RouterContextValue } from '@rocket.chat/ui-contexts'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Tracker } from 'meteor/tracker'; import React, { FC } from 'react'; -const createSubscription = function <T>(getValue: () => T): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T] { - let currentValue = Tracker.nonreactive(getValue); - return [ - (callback: () => void): (() => void) => { - const computation = Tracker.autorun(() => { - currentValue = getValue(); - Tracker.afterFlush(callback); - }); - - return (): void => { - computation.stop(); - }; - }, - (): T => currentValue, - ]; -}; +import { createSubscription } from '../lib/createSubscription'; const queryRoutePath = ( name: Parameters<RouterContextValue['queryRoutePath']>[0], diff --git a/apps/meteor/client/providers/SessionProvider.tsx b/apps/meteor/client/providers/SessionProvider.tsx index 447cd93c806..05556f5e172 100644 --- a/apps/meteor/client/providers/SessionProvider.tsx +++ b/apps/meteor/client/providers/SessionProvider.tsx @@ -2,7 +2,7 @@ import { SessionContext } from '@rocket.chat/ui-contexts'; import { Session } from 'meteor/session'; import React, { FC } from 'react'; -import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; +import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; const contextValue = { query: createReactiveSubscriptionFactory<unknown>((name) => Session.get(name)), diff --git a/apps/meteor/client/providers/SettingsProvider.tsx b/apps/meteor/client/providers/SettingsProvider.tsx index 0ed442dabc1..e270ac8e18f 100644 --- a/apps/meteor/client/providers/SettingsProvider.tsx +++ b/apps/meteor/client/providers/SettingsProvider.tsx @@ -2,9 +2,9 @@ import { SettingsContext, SettingsContextValue, useAtLeastOnePermission, useMeth import { Tracker } from 'meteor/tracker'; import React, { useCallback, useEffect, useMemo, useState, FunctionComponent } from 'react'; +import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; import { PrivateSettingsCachedCollection } from '../lib/settings/PrivateSettingsCachedCollection'; import { PublicSettingsCachedCollection } from '../lib/settings/PublicSettingsCachedCollection'; -import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; type SettingsProviderProps = { readonly privileged?: boolean; diff --git a/apps/meteor/client/providers/UserProvider.tsx b/apps/meteor/client/providers/UserProvider.tsx index 223d0af3aa5..c8a7fdc53ba 100644 --- a/apps/meteor/client/providers/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider.tsx @@ -7,7 +7,7 @@ import { Subscriptions, Rooms } from '../../app/models/client'; import { getUserPreference } from '../../app/utils/client'; import { callbacks } from '../../lib/callbacks'; import { useReactiveValue } from '../hooks/useReactiveValue'; -import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory'; +import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; const getUserId = (): string | null => Meteor.userId(); diff --git a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx index 60512a59197..4ea376af684 100644 --- a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx +++ b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx @@ -7,7 +7,7 @@ import type { FilterOperators } from 'mongodb'; import React, { useEffect, useMemo, FunctionComponent, useRef, MutableRefObject } from 'react'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; -import { createReactiveSubscriptionFactory } from '../../../providers/createReactiveSubscriptionFactory'; +import { createReactiveSubscriptionFactory } from '../../../lib/createReactiveSubscriptionFactory'; import { EditableSettingsContext, EditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; const defaultQuery: SettingsContextQuery = {}; -- GitLab From 1e1b47e8a2ac57b394ff33e1408b244d95731057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <henrique.jobs1@gmail.com> Date: Fri, 16 Sep 2022 15:18:05 -0300 Subject: [PATCH 046/107] Chore: Change BundleChips component appearance (#26686) --- .../meteor/client/views/admin/apps/AppRow.tsx | 15 ++++------ .../client/views/admin/apps/AppsPage.tsx | 8 +++++- .../client/views/admin/apps/BundleChips.tsx | 28 +++---------------- .../rocketchat-i18n/i18n/en.i18n.json | 1 - 4 files changed, 16 insertions(+), 36 deletions(-) diff --git a/apps/meteor/client/views/admin/apps/AppRow.tsx b/apps/meteor/client/views/admin/apps/AppRow.tsx index a31f57c83e1..f71fe26f2b5 100644 --- a/apps/meteor/client/views/admin/apps/AppRow.tsx +++ b/apps/meteor/client/views/admin/apps/AppRow.tsx @@ -1,6 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import { useMediaQueries } from '@rocket.chat/fuselage-hooks'; +import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { useRoute } from '@rocket.chat/ui-contexts'; import React, { FC, memo, KeyboardEvent, MouseEvent } from 'react'; @@ -15,11 +15,8 @@ const AppRow: FC<App & { isMarketplace: boolean }> = (props) => { const { name, id, description, iconFileData, marketplaceVersion, iconFileContent, installed, isSubscribed, isMarketplace, bundledIn } = props; - const [isAppNameTruncated, isBundleTextVisible, isDescriptionVisible] = useMediaQueries( - '(max-width: 510px)', - '(max-width: 887px)', - '(min-width: 1200px)', - ); + const breakpoints = useBreakpoints(); + const isDescriptionVisible = breakpoints.includes('xl'); const appsRoute = useRoute('admin-apps'); const marketplaceRoute = useRoute('admin-marketplace'); @@ -83,14 +80,12 @@ const AppRow: FC<App & { isMarketplace: boolean }> = (props) => { <Box display='flex' flexDirection='row' width='80%'> <AppAvatar size='x40' mie='x16' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData} /> <Box display='flex' alignItems='center' color='default' fontScale='p2m' mie='x16' style={{ whiteSpace: 'nowrap' }}> - <Box is='span' withTruncatedText={isAppNameTruncated}> - {name} - </Box> + <Box is='span'>{name}</Box> </Box> <Box display='flex' mie='x16' alignItems='center' color='default'> {bundledIn && Boolean(bundledIn.length) && ( <Box display='flex' alignItems='center' color='default' mie='x16'> - <BundleChips bundledIn={bundledIn} isIconOnly={isBundleTextVisible} /> + <BundleChips bundledIn={bundledIn} /> </Box> )} {isDescriptionVisible && ( diff --git a/apps/meteor/client/views/admin/apps/AppsPage.tsx b/apps/meteor/client/views/admin/apps/AppsPage.tsx index ac3e221101c..f716e34d39e 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage.tsx @@ -61,7 +61,13 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { <Tabs.Item onClick={(): void => marketplaceRoute.push({ context: '' })} selected={isMarketplace}> {t('Marketplace')} </Tabs.Item> - <Tabs.Item onClick={(): void => marketplaceRoute.push({ context: 'installed' })} selected={!isMarketplace}> + <Tabs.Item + onClick={(): void => marketplaceRoute.push({ context: 'installed' })} + selected={!isMarketplace} + mbe='neg-x4' + borderWidth='0' + borderBlockWidth='x4' + > {t('Installed')} </Tabs.Item> </Tabs> diff --git a/apps/meteor/client/views/admin/apps/BundleChips.tsx b/apps/meteor/client/views/admin/apps/BundleChips.tsx index 73aabdb4293..7150893f802 100644 --- a/apps/meteor/client/views/admin/apps/BundleChips.tsx +++ b/apps/meteor/client/views/admin/apps/BundleChips.tsx @@ -1,4 +1,4 @@ -import { Box, Icon, PositionAnimated, AnimatedVisibility, Tooltip } from '@rocket.chat/fuselage'; +import { Box, PositionAnimated, AnimatedVisibility, Tooltip, Tag } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { RefObject, useRef, useState, ReactElement, Fragment } from 'react'; @@ -10,10 +10,9 @@ type BundleChipsProps = { bundleName: string; apps: App[]; }[]; - isIconOnly?: boolean; }; -const BundleChips = ({ bundledIn, isIconOnly }: BundleChipsProps): ReactElement => { +const BundleChips = ({ bundledIn }: BundleChipsProps): ReactElement => { const t = useTranslation(); const bundleRef = useRef<Element>(); @@ -23,27 +22,8 @@ const BundleChips = ({ bundledIn, isIconOnly }: BundleChipsProps): ReactElement <> {bundledIn.map((bundle) => ( <Fragment key={bundle.bundleId}> - <Box - display='flex' - flexDirection='row' - alignItems='center' - justifyContent='center' - backgroundColor='disabled' - pi='x4' - height='x20' - borderRadius='x2' - ref={bundleRef} - onMouseEnter={(): void => setIsHovered(true)} - onMouseLeave={(): void => setIsHovered(false)} - > - <Icon name='bag' size='x20' /> - {!isIconOnly && ( - <Box fontScale='c2' color='info' style={{ whiteSpace: 'nowrap' }}> - {t('bundle_chip_title', { - bundleName: bundle.bundleName, - })} - </Box> - )} + <Box ref={bundleRef} onMouseEnter={(): void => setIsHovered(true)} onMouseLeave={(): void => setIsHovered(false)}> + <Tag variant='primary'>{bundle.bundleName}</Tag> </Box> <PositionAnimated anchor={bundleRef as RefObject<Element>} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 5b0a79ebc00..32beb4abae3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -748,7 +748,6 @@ "Build_Environment": "Build Environment", "bulk-register-user": "Bulk Create Users", "bulk-register-user_description": "Permission to create users in bulk", - "bundle_chip_title": "__bundleName__ Bundle", "Bundles": "Bundles", "Busiest_day": "Busiest Day", "Busiest_time": "Busiest Time", -- GitLab From 7905852c2c1185fa94404e80535508ac9185b2a3 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Mon, 19 Sep 2022 14:30:49 -0300 Subject: [PATCH 047/107] [IMPROVE] Updating voip tooltips and icons (#26834) --- .../footer/voip/VoipFooter.stories.tsx | 9 ----- .../client/sidebar/footer/voip/VoipFooter.tsx | 38 +++++++++---------- .../client/sidebar/footer/voip/index.tsx | 9 ----- .../sidebar/sections/OmnichannelSection.tsx | 31 ++++----------- .../actions/OmnichannelCallDialPad.tsx | 2 +- .../actions/OmnichannelCallToggleError.tsx | 2 +- .../actions/OmnichannelCallToggleLoading.tsx | 2 +- .../actions/OmnichannelCallToggleReady.tsx | 20 ++++------ .../actions/OmnichannelLivechatToggle.tsx | 30 +++++++++++++++ .../client/sidebar/sections/actions/index.ts | 6 +++ .../rocketchat-i18n/i18n/de.i18n.json | 1 - .../rocketchat-i18n/i18n/en.i18n.json | 15 +++++--- .../rocketchat-i18n/i18n/pl.i18n.json | 1 - .../rocketchat-i18n/i18n/pt-BR.i18n.json | 13 ++++++- 14 files changed, 93 insertions(+), 86 deletions(-) create mode 100644 apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx create mode 100644 apps/meteor/client/sidebar/sections/actions/index.ts diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx index c8d273305a7..0cce790a2e0 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.stories.tsx @@ -15,14 +15,6 @@ const callActions = { reject: () => ({}), }; -const tooltips = { - mute: 'Mute', - holdCall: 'Hold Call', - holdCallEEOnly: 'Hold Call (Enterprise Edition only)', - acceptCall: 'Accept Call', - endCall: 'End Call', -}; - const callerDefault = { callerName: '', callerId: '+5551999999999', @@ -84,7 +76,6 @@ const VoipFooterTemplate: ComponentStory<typeof VoipFooter> = (args) => { paused={paused} toggleMic={toggleMic} togglePause={togglePause} - tooltips={tooltips} createRoom={async () => ''} openRoom={() => ''} callsInQueue='2 Calls In Queue' diff --git a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx index 1179921a552..7904a950183 100644 --- a/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx +++ b/apps/meteor/client/sidebar/footer/voip/VoipFooter.tsx @@ -19,15 +19,7 @@ type VoipFooterPropsType = { paused: boolean; toggleMic: (state: boolean) => void; togglePause: (state: boolean) => void; - tooltips: { - mute: string; - holdCall: string; - holdCallEEOnly: string; - acceptCall: string; - endCall: string; - }; callsInQueue: string; - createRoom: (caller: ICallerInfo, callDirection?: IVoipRoom['direction']) => Promise<IVoipRoom['_id']>; openRoom: (rid: IVoipRoom['_id']) => void; dispatchEvent: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void; @@ -47,7 +39,6 @@ export const VoipFooter = ({ paused, toggleMic, togglePause, - tooltips, createRoom, openRoom, callsInQueue, @@ -74,6 +65,13 @@ export const VoipFooter = ({ togglePause(!paused); }; + const holdTitle = ((): string => { + if (!isEnterprise) { + return t('Hold_EE_only'); + } + return paused ? t('Resume') : t('Hold'); + })(); + return ( <SidebarFooter elevated> <Box @@ -94,25 +92,25 @@ export const VoipFooter = ({ {(callerState === 'IN_CALL' || callerState === 'ON_HOLD') && ( <ButtonGroup medium className='sidebar--custom-colors'> <IconButton + small disabled={paused} - title={tooltips.mute} + icon={muted ? 'mic-off' : 'mic'} color={muted ? 'neutral-500' : 'info'} - icon='mic' - small + data-tooltip={muted ? t('Turn_on_microphone') : t('Turn_off_microphone')} onClick={(e): void => { e.stopPropagation(); toggleMic(!muted); }} /> <IconButton - title={isEnterprise ? tooltips.holdCall : tooltips.holdCallEEOnly} + small + data-tooltip={holdTitle} disabled={!isEnterprise} - icon='pause-unfilled' + icon={paused ? 'pause' : 'pause-unfilled'} color={paused ? 'neutral-500' : 'info'} - small onClick={handleHold} /> - {options && <Menu color='neutral-500' options={options} />} + {options && <Menu color='neutral-500' data-tooltip={t('More_options')} options={options} />} </ButtonGroup> )} </Box> @@ -129,12 +127,12 @@ export const VoipFooter = ({ <ButtonGroup medium> {(callerState === 'IN_CALL' || callerState === 'ON_HOLD' || callerState === 'OFFER_SENT') && ( <Button - title={tooltips.endCall} - disabled={paused} small square danger + disabled={paused} aria-label={t('End_call')} + data-tooltip={t('End_Call')} onClick={(e): unknown => { e.stopPropagation(); muted && toggleMic(false); @@ -146,16 +144,16 @@ export const VoipFooter = ({ </Button> )} {callerState === 'OFFER_RECEIVED' && ( - <Button title={tooltips.endCall} aria-label={t('Reject_call')} small square danger onClick={callActions.reject}> + <Button data-tooltip={t('Decline')} aria-label={t('Decline')} small square danger onClick={callActions.reject}> <Icon name='phone-off' size='x16' /> </Button> )} {callerState === 'OFFER_RECEIVED' && ( <Button - title={tooltips.acceptCall} small square success + data-tooltip={t('Accept')} onClick={async (): Promise<void> => { callActions.pickUp(); const rid = await createRoom(caller); diff --git a/apps/meteor/client/sidebar/footer/voip/index.tsx b/apps/meteor/client/sidebar/footer/voip/index.tsx index 17a327fdd6d..6e9d78d62c6 100644 --- a/apps/meteor/client/sidebar/footer/voip/index.tsx +++ b/apps/meteor/client/sidebar/footer/voip/index.tsx @@ -61,14 +61,6 @@ export const VoipFooter = (): ReactElement | null => { return subtitles[state] || ''; }; - const tooltips = { - mute: t('Mute'), - holdCall: t('Hold_Call'), - holdCallEEOnly: t('Hold_Call_EE_only'), - acceptCall: t('Accept_Call'), - endCall: t('End_Call'), - }; - const getCallsInQueueText = useMemo((): string => { if (queueCounter === 0) { return t('Calls_in_queue_empty'); @@ -96,7 +88,6 @@ export const VoipFooter = (): ReactElement | null => { paused={paused} toggleMic={toggleMic} togglePause={togglePause} - tooltips={tooltips} createRoom={createRoom} openRoom={openRoom} callsInQueue={getCallsInQueueText} diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx index e411c68bc45..96a70c8ffbc 100644 --- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx +++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx @@ -1,40 +1,21 @@ import { Box, Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useLayout, useToastMessageDispatch, useRoute, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useLayout, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, ReactElement } from 'react'; import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; -import { useOmnichannelAgentAvailable } from '../../hooks/omnichannel/useOmnichannelAgentAvailable'; import { useOmnichannelShowQueueLink } from '../../hooks/omnichannel/useOmnichannelShowQueueLink'; -import { OmniChannelCallDialPad } from './actions/OmnichannelCallDialPad'; -import { OmnichannelCallToggle } from './actions/OmnichannelCallToggle'; +import { OmniChannelCallDialPad, OmnichannelCallToggle, OmnichannelLivechatToggle } from './actions'; const OmnichannelSection = (props: typeof Box): ReactElement => { const t = useTranslation(); - const changeAgentStatus = useMethod('livechat:changeLivechatStatus'); const isCallEnabled = useIsCallEnabled(); const isCallReady = useIsCallReady(); const hasPermissionToSeeContactCenter = usePermission('view-omnichannel-contact-center'); - const agentAvailable = useOmnichannelAgentAvailable(); const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); const { sidebar } = useLayout(); const directoryRoute = useRoute('omnichannel-directory'); const queueListRoute = useRoute('livechat-queue'); - const dispatchToastMessage = useToastMessageDispatch(); - - const omnichannelIcon = { - title: agentAvailable ? t('Available') : t('Not_Available'), - color: agentAvailable ? 'success' : undefined, - icon: agentAvailable ? 'message' : 'message-disabled', - } as const; - - const handleAvailableStatusChange = useMutableCallback(async () => { - try { - await changeAgentStatus(); - } catch (error: unknown) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); const handleRoute = useMutableCallback((route) => { sidebar.toggle(); @@ -54,11 +35,13 @@ const OmnichannelSection = (props: typeof Box): ReactElement => { <Sidebar.TopBar.ToolBox className='omnichannel-sidebar' {...props}> <Sidebar.TopBar.Title>{t('Omnichannel')}</Sidebar.TopBar.Title> <Sidebar.TopBar.Actions> - {showOmnichannelQueueLink && <Sidebar.TopBar.Action icon='queue' title={t('Queue')} onClick={(): void => handleRoute('queue')} />} + {showOmnichannelQueueLink && ( + <Sidebar.TopBar.Action icon='queue' data-tooltip={t('Queue')} onClick={(): void => handleRoute('queue')} /> + )} {isCallEnabled && <OmnichannelCallToggle />} - <Sidebar.TopBar.Action {...omnichannelIcon} onClick={handleAvailableStatusChange} /> + <OmnichannelLivechatToggle /> {hasPermissionToSeeContactCenter && ( - <Sidebar.TopBar.Action title={t('Contact_Center')} icon='address-book' onClick={(): void => handleRoute('directory')} /> + <Sidebar.TopBar.Action data-tooltip={t('Contact_Center')} icon='address-book' onClick={(): void => handleRoute('directory')} /> )} {isCallReady && <OmniChannelCallDialPad />} </Sidebar.TopBar.Actions> diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx index d6146db21a8..9d0c4f6512c 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallDialPad.tsx @@ -14,11 +14,11 @@ export const OmniChannelCallDialPad = ({ ...props }): ReactElement => { return ( <Sidebar.TopBar.Action - title={outBoundCallsAllowed ? t('New_Call') : t('New_Call_Enterprise_Edition_Only')} icon='dialpad' onClick={(): void => openDialModal()} disabled={!outBoundCallsEnabledForUser} aria-label={t('Open_Dialpad')} + data-tooltip={outBoundCallsAllowed ? t('New_Call') : t('New_Call_Enterprise_Edition_Only')} {...props} /> ); diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx index 0d5d7817f9c..6357e2a6fb6 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleError.tsx @@ -4,5 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleError = ({ ...props }): ReactElement => { const t = useTranslation(); - return <Sidebar.TopBar.Action icon='phone' danger data-title={t('Error')} disabled {...props} />; + return <Sidebar.TopBar.Action icon='phone' danger data-tooltip={t('Error')} disabled {...props} />; }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx index 89048d57763..04ea607e650 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleLoading.tsx @@ -4,5 +4,5 @@ import React, { ReactElement } from 'react'; export const OmnichannelCallToggleLoading = ({ ...props }): ReactElement => { const t = useTranslation(); - return <Sidebar.TopBar.Action icon='phone' data-title={t('Loading')} aria-label={t('VoIP_Toggle')} disabled {...props} />; + return <Sidebar.TopBar.Action icon='phone' data-tooltip={t('Loading')} aria-label={t('VoIP_Toggle')} disabled {...props} />; }; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx index 7f285da22c5..7ed412cfc33 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx @@ -25,7 +25,7 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { const getTitle = (): string => { if (networkStatus === 'offline') { - return t('Signaling_connection_disconnected'); + return t('Waiting_for_server_connection'); } if (inCall) { @@ -33,10 +33,10 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { } if (registered) { - return t('Enabled'); + return t('Turn_off_answer_calls'); } - return t('Disabled'); + return t('Turn_on_answer_calls'); }; const getIcon = (): 'phone-issue' | 'phone' | 'phone-disabled' => { @@ -53,18 +53,14 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { return registered ? 'success' : undefined; }; - const voipCallIcon = { - title: getTitle(), - icon: getIcon(), - color: getColor(), - }; - return ( <Sidebar.TopBar.Action - aria-label={t('VoIP_Toggle')} - aria-checked={registered} + icon={getIcon()} disabled={inCall} - {...voipCallIcon} + color={getColor()} + aria-checked={registered} + aria-label={t('VoIP_Toggle')} + data-tooltip={getTitle()} {...props} onClick={onClickVoipButton} /> diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx new file mode 100644 index 00000000000..6a1aee2149e --- /dev/null +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx @@ -0,0 +1,30 @@ +import { Sidebar } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import { useOmnichannelAgentAvailable } from '../../../hooks/omnichannel/useOmnichannelAgentAvailable'; + +export const OmnichannelLivechatToggle = (): ReactElement => { + const t = useTranslation(); + const agentAvailable = useOmnichannelAgentAvailable(); + const changeAgentStatus = useMethod('livechat:changeLivechatStatus'); + const dispatchToastMessage = useToastMessageDispatch(); + + const handleAvailableStatusChange = useMutableCallback(async () => { + try { + await changeAgentStatus(); + } catch (error: unknown) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + return ( + <Sidebar.TopBar.Action + data-tooltip={agentAvailable ? t('Turn_off_answer_chats') : t('Turn_on_answer_chats')} + color={agentAvailable ? 'success' : undefined} + icon={agentAvailable ? 'message' : 'message-disabled'} + onClick={handleAvailableStatusChange} + /> + ); +}; diff --git a/apps/meteor/client/sidebar/sections/actions/index.ts b/apps/meteor/client/sidebar/sections/actions/index.ts new file mode 100644 index 00000000000..d74426b43c6 --- /dev/null +++ b/apps/meteor/client/sidebar/sections/actions/index.ts @@ -0,0 +1,6 @@ +export * from './OmnichannelCallDialPad'; +export * from './OmnichannelCallToggle'; +export * from './OmnichannelCallToggleError'; +export * from './OmnichannelCallToggleError'; +export * from './OmnichannelCallToggleReady'; +export * from './OmnichannelLivechatToggle'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index dbf2cc5b21c..174e3293e66 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -4201,7 +4201,6 @@ "Sidebar": "Seitenleiste", "Sidebar_list_mode": "Seitenleisten-Channel-Listen-Modus", "Sign_in_to_start_talking": "Anmelden, um mit dem Chatten zu beginnen", - "Signaling_connection_disconnected": "Signalisierungsverbindung unterbrochen", "since_creation": "seit %s", "Site_Name": "Seitenname", "Site_Url": "Website-URL", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 32beb4abae3..582829ff286 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -26,7 +26,6 @@ "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "A new owner will be assigned automatically to the <span style=\"font-weight: bold;\">__roomName__</span> room.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "A new owner will be assigned automatically to those <span style=\"font-weight: bold;\">__count__</span> rooms:<br/> __rooms__.", "Accept": "Accept", - "Accept_Call": "Accept Call", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accept incoming omnichannel requests even if there are no online agents", "Accept_new_livechats_when_agent_is_idle": "Accept new omnichannel requests when the agent is idle", "Accept_with_no_online_agents": "Accept with No Online Agents", @@ -2319,8 +2318,8 @@ "Highlights_List": "Highlight words", "History": "History", "Hold_Time": "Hold Time", - "Hold_Call": "Hold Call", - "Hold_Call_EE_only": "Hold Call (Enterprise Edition only)", + "Hold": "Hold", + "Hold_EE_only": "Hold (Enterprise Edition only)", "Home": "Home", "Homepage": "Homepage", "Host": "Host", @@ -4331,7 +4330,6 @@ "Sidebar": "Sidebar", "Sidebar_list_mode": "Sidebar Channel List Mode", "Sign_in_to_start_talking": "Sign in to start talking", - "Signaling_connection_disconnected": "Signaling connection disconnected", "since_creation": "since %s", "Site_Name": "Site Name", "Site_Url": "Site URL", @@ -4783,6 +4781,12 @@ "Turn_OFF": "Turn OFF", "Turn_ON": "Turn ON", "Turn_on_video": "Turn on video", + "Turn_on_answer_chats": "Turn on answer chats", + "Turn_on_answer_calls": "Turn on answer calls", + "Turn_on_microphone": "Turn on microphone", + "Turn_off_microphone": "Turn off microphone", + "Turn_off_answer_chats": "Turn off answer chats", + "Turn_off_answer_calls": "Turn off answer calls", "Turn_off_video": "Turn off video", "Two Factor Authentication": "Two Factor Authentication", "Two-factor_authentication": "Two-factor authentication via TOTP", @@ -5211,6 +5215,7 @@ "Waiting_queue_message": "Waiting queue message", "Waiting_queue_message_description": "Message that will be displayed to the visitors when they get in the queue", "Waiting_Time": "Waiting Time", + "Waiting_for_server_connection": "Waiting for server connection", "Warning": "Warning", "Warnings": "Warnings", "WAU_value": "WAU __value__", @@ -5442,4 +5447,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Login Detected", "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", "Something_Went_Wrong": "Something went wrong" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 7785e22ad44..29c947f3aaf 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -4292,7 +4292,6 @@ "Sidebar": "Pasek boczny", "Sidebar_list_mode": "Tryb listy kanałów na pasku bocznym", "Sign_in_to_start_talking": "Zaloguj siÄ™, aby zacząć mówić", - "Signaling_connection_disconnected": "PoÅ‚Ä…czenie sygnalizacyjne rozÅ‚Ä…czone", "since_creation": "od %s", "Site_Name": "Nazwa strony", "Site_Url": "Adres strony", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index f2cc4d44e6a..e39222b4acf 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2144,7 +2144,8 @@ "Highlights_List": "Destacar palavras", "History": "Histórico", "Hold_Time": "Tempo de espera", - "Hold_Call": "Pausar chamada", + "Hold": "Pausar", + "Hold_EE_only": "Pausar (Enterprise Edition only)", "Home": "InÃcio", "Host": "Host", "Hospitality_Businness": "Hospitalidade", @@ -3620,6 +3621,7 @@ "Restart": "Reiniciar", "Restart_the_server": "Reiniciar o servidor", "restart-server": "Reiniciar o servidor", + "Resume": "Retomar", "Retail": "Varejo", "Retention_setting_changed_successfully": "A configuração da polÃtica de retenção foi alterada com sucesso", "RetentionPolicy": "PolÃtica de retenção", @@ -4397,6 +4399,12 @@ "Turn_OFF": "Desligar", "Turn_ON": "Ligar", "Turn_on_video": "Ligar o vÃdeo", + "Turn_on_answer_chats": "Habilitar receber chats", + "Turn_on_answer_calls": "Habilitar ligações", + "Turn_on_microphone": "Habilitar microfone", + "Turn_off_microphone": "Desabilitar microfone", + "Turn_off_answer_chats": "Desabilitar receber chats", + "Turn_off_answer_calls": "Desabilitar ligações", "Turn_off_video": "Desligar o vÃdeo", "Two Factor Authentication": "Autenticação de dois fatores", "Two-factor_authentication": "Autenticação de dois fatores por TOTP", @@ -4755,6 +4763,7 @@ "Waiting_queue_message": "Mensagem de fila de espera", "Waiting_queue_message_description": "Mensagem que será exibida aos visitantes quando eles entrarem na fila de espera", "Waiting_Time": "Tempo de espera", + "Waiting_for_server_connection": "Aguardando conexão com o servidor", "Warning": "Aviso", "Warnings": "Avisos", "WAU_value": "WAU __value__", @@ -4958,4 +4967,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponÃveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "É necessário integrar manualmente com serviços externos" -} \ No newline at end of file +} -- GitLab From 52437f82acc9d519e466b4d6e4fdfb112157334a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 19 Sep 2022 17:23:15 -0300 Subject: [PATCH 048/107] [FIX] Upload fails when using WebDav as file storage (#26711) --- .../app/file-upload/ufs/Webdav/server.js | 4 ++-- .../webdav/server/lib/webdavClientAdapter.ts | 20 +++++++++++++++++-- apps/meteor/package.json | 2 +- yarn.lock | 14 ++++++------- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/file-upload/ufs/Webdav/server.js b/apps/meteor/app/file-upload/ufs/Webdav/server.js index c635e38eb58..7b2f8b95b64 100644 --- a/apps/meteor/app/file-upload/ufs/Webdav/server.js +++ b/apps/meteor/app/file-upload/ufs/Webdav/server.js @@ -76,7 +76,7 @@ export class WebdavStore extends UploadFS.Store { .then((data) => { callback && callback(null, data); }) - .catch(SystemLogger.error); + .catch((...args) => SystemLogger.error(...args)); }; /** @@ -107,7 +107,7 @@ export class WebdavStore extends UploadFS.Store { */ this.getWriteStream = function (fileId, file) { const writeStream = new stream.PassThrough(); - const webdavStream = client.createWriteStream(this.getPath(file)); + const webdavStream = client.createWriteStream(this.getPath(file), file.size || 0); // TODO remove timeout when UploadFS bug resolved const newListenerCallback = (event, listener) => { diff --git a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts b/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts index f8462802bc6..43e9bb4642d 100644 --- a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts +++ b/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts @@ -1,3 +1,4 @@ +import stream from 'stream'; import type { Readable, Writable } from 'stream'; import type { WebDAVClient, FileStat, ResponseDataDetailed, WebDAVClientOptions } from 'webdav'; @@ -69,7 +70,22 @@ export class WebdavClientAdapter { return this._client.createReadStream(path, options); } - createWriteStream(path: string): Writable { - return this._client.createWriteStream(path); + createWriteStream(path: string, fileSize: number): Writable { + const ws = new stream.PassThrough(); + + this._client + .customRequest(path, { + method: 'PUT', + headers: { + ...(fileSize ? { 'Content-Length': String(fileSize) } : {}), + }, + data: ws, + maxRedirects: 0, + }) + .catch((err) => { + ws.emit('error', err); + }); + + return ws; } } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 0de11be17c0..1ad17f14861 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -377,7 +377,7 @@ "use-sync-external-store": "^1.2.0", "uuid": "^8.3.2", "vm2": "^3.9.10", - "webdav": "^4.10.0", + "webdav": "^4.11.0", "xml-crypto": "^2.1.4", "xml-encryption": "2.0.0", "xml2js": "0.4.23", diff --git a/yarn.lock b/yarn.lock index c875ce41cde..54e3eb74e7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6354,7 +6354,7 @@ __metadata: use-sync-external-store: ^1.2.0 uuid: ^8.3.2 vm2: ^3.9.10 - webdav: ^4.10.0 + webdav: ^4.11.0 webpack: ^4.46.0 xml-crypto: ^2.1.4 xml-encryption: 2.0.0 @@ -25943,7 +25943,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1": +"minimatch@npm:^5.1.0": version: 5.1.0 resolution: "minimatch@npm:5.1.0" dependencies: @@ -36270,9 +36270,9 @@ __metadata: languageName: node linkType: hard -"webdav@npm:^4.10.0": - version: 4.10.0 - resolution: "webdav@npm:4.10.0" +"webdav@npm:^4.11.0": + version: 4.11.0 + resolution: "webdav@npm:4.11.0" dependencies: axios: ^0.27.2 base-64: ^1.0.0 @@ -36282,12 +36282,12 @@ __metadata: hot-patcher: ^0.5.0 layerr: ^0.1.2 md5: ^2.3.0 - minimatch: ^5.0.1 + minimatch: ^5.1.0 nested-property: ^4.0.0 path-posix: ^1.0.0 url-join: ^4.0.1 url-parse: ^1.5.10 - checksum: 0cfea9c233cfb8c490b6f6be01cb789f20fd01dbfe0ebc8f89057000390117a7cdbd55501c5fc248f54f2393d529cc60a058bf982d96e7e2507a7c99528444ab + checksum: 0a7aab0a3118deea20485d3c417fda40e71c3930b866a92cfff5dedce4a25c0fbed0cd083bf14ac5a4ee7cab55136265d5011b2601cbb2a1b359da551910ec97 languageName: node linkType: hard -- GitLab From 30230229608a442ff63614cb92cb62df89bb3ec8 Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 15:25:59 +0000 Subject: [PATCH 049/107] =?UTF-8?q?i18n:=20Language=20update=20from=20Ling?= =?UTF-8?q?oHub=20=F0=9F=A4=96=20on=202022-09-19Z=20(#26896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 4 ++-- apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 ++- apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json | 1 + apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json | 1 + 55 files changed, 57 insertions(+), 5 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json index d08cbd123e1..67d52f6003b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json @@ -1434,6 +1434,7 @@ "It_works": "Dit werk", "italics": "kursief", "Job_Title": "Werkstitel", + "Join": "aansluit", "Join_audio_call": "Sluit aan by klankoproep", "Join_Chat": "Sluit aan by Chat", "Join_default_channels": "Sluit by verstek kanale aan", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index 4091d89128e..d6fe2e12993 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -2401,6 +2401,7 @@ "italics": "مائل", "Items_per_page:": "عناصر لكل صÙØØ©:", "Job_Title": "المسمى الوظيÙÙŠ", + "Join": "انضمام", "Join_audio_call": "انضمام إلى مكالمة صوتية", "Join_call": "انضمام إلى مكالمة", "Join_Chat": "انضمام إلى دردشة", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json index 102732abf51..9e7da93b87e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json @@ -1434,6 +1434,7 @@ "It_works": "Ä°ÅŸlÉ™yir", "italics": "italik", "Job_Title": "VÉ™zifÉ™", + "Join": "QoÅŸulun", "Join_audio_call": "Audio zÉ™ngÉ™ qoÅŸulun", "Join_Chat": "SohbetÉ™ qoÅŸul", "Join_default_channels": "Standart kanallara qoÅŸulun", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json index 0e1ff0ba748..9a5deee6844 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json @@ -1450,6 +1450,7 @@ "It_works": "ГÑта працуе", "italics": "курÑÑ–Ñž", "Job_Title": "ПаÑада", + "Join": "далучыцца", "Join_audio_call": "Ð ÑгіÑÑ‚Ñ€Ð°Ñ†Ñ‹Ñ Ð°ÑžÐ´Ñ‹Ñ‘ званка", "Join_Chat": "УвайÑці Ñž чат", "Join_default_channels": "Ð ÑгіÑÑ‚Ñ€Ð°Ñ†Ñ‹Ñ ÐºÐ°Ð½Ð°Ð»Ð°Ñž па змаўчанні", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json index f03fb92e3c2..5d968b448e3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json @@ -1432,6 +1432,7 @@ "It_works": "работи", "italics": "курÑив", "Job_Title": "ДлъжноÑÑ‚", + "Join": "ПриÑъедините", "Join_audio_call": "Включете аудио повикване", "Join_Chat": "Влезте в чата", "Join_default_channels": "ПриÑъединÑване към каналите по подразбиране", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json index 2788d5a0020..1f9a2979297 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json @@ -1429,6 +1429,7 @@ "It_works": "Radi", "italics": "ukosi", "Job_Title": "Naziv posla", + "Join": "Pridruži se", "Join_audio_call": "Pridružite se audiopozivu", "Join_Chat": "Pridružite se Chatu", "Join_default_channels": "Pridružite se zadanim kanalima", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index 7c95d9bcb53..e939943f8b6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -2376,6 +2376,7 @@ "italics": "cursiva", "Items_per_page:": "Elements per pà gina:", "Job_Title": "TÃtol professional", + "Join": "Unir-se", "Join_audio_call": "Unir-se a la trucada", "Join_call": "Unir-se a la trucada", "Join_Chat": "Uneix-te al xat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index c413ef5bf0e..e058d94d916 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -2030,6 +2030,7 @@ "italics": "kurzÃva", "Items_per_page:": "Položek na stránku:", "Job_Title": "Název pozice", + "Join": "PÅ™ipojit", "Join_audio_call": "PÅ™ipojit k hovoru", "Join_Chat": "PÅ™ipojit k chatu", "Join_default_channels": "PÅ™ipojit se k výchozÃm mÃstnostem", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json index 34387a0ba1e..f5f0b429938 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json @@ -1430,6 +1430,7 @@ "It_works": "Mae'n gweithio", "italics": "italig", "Job_Title": "Teitl swydd", + "Join": "Ymunwch", "Join_audio_call": "Ymunwch â galwad sain", "Join_Chat": "Ymunwch â Chat", "Join_default_channels": "Ymunwch â sianeli diofyn", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index cf996168b6b..22301c87a4a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -2040,6 +2040,7 @@ "italics": "kursiv", "Items_per_page:": "Enheder pr. side:", "Job_Title": "Jobtitel", + "Join": "Tilslut", "Join_audio_call": "Deltag i lydopkald", "Join_Chat": "Tilmeld dig chat", "Join_default_channels": "Tilmeld dig standardkanaler", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json index c981c8fd42a..96ffd4c81b1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -1435,6 +1435,7 @@ "It_works": "Funktioniert!", "italics": "kursiv", "Job_Title": "Berufsbezeichnung", + "Join": "Beitreten", "Join_audio_call": "Anruf beitreiten", "Join_Chat": "Chat beitreten", "Join_default_channels": "Standardkanälen beitreten", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 174e3293e66..5b5b8bef547 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -737,7 +737,6 @@ "Build_Environment": "Build-Umgebung", "bulk-register-user": "Massen-Registrierung von Benutzern", "bulk-register-user_description": "Berechtigung zur Massen-Registrierung von Benutzern", - "bundle_chip_title": "__bundleName__ Bundle", "Bundles": "Bundle", "Busiest_day": "Geschäftigster Tag", "Busiest_time": "Geschäftigste Zeit", @@ -2535,6 +2534,7 @@ "italics": "kursiv", "Items_per_page:": "Artikel pro Seite:", "Job_Title": "Berufsbezeichnung", + "Join": "Beitreten", "Join_audio_call": "Anruf beitreten", "Join_call": "Anruf beitreten", "Join_Chat": "Chat beitreten", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json index 0183f1e36ca..a46d017c83e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json @@ -1441,6 +1441,7 @@ "It_works": "ΔουλεÏει", "italics": "πλάγια", "Job_Title": "Τίτλος εÏγασίας", + "Join": "Γίνε μÎλος", "Join_audio_call": "Συμμετοχή στην κλήση ήχου", "Join_Chat": "Συμμετοχή σε συζήτηση", "Join_default_channels": "Γίνετε μÎλος Ï€Ïοεπιλογή κανάλια", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 582829ff286..4356bb33902 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2318,7 +2318,7 @@ "Highlights_List": "Highlight words", "History": "History", "Hold_Time": "Hold Time", - "Hold": "Hold", + "Hold": "Hold", "Hold_EE_only": "Hold (Enterprise Edition only)", "Home": "Home", "Homepage": "Homepage", @@ -5447,4 +5447,4 @@ "Device_Management_Email_Subject": "[Site_Name] - Login Detected", "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", "Something_Went_Wrong": "Something went wrong" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json index 92bdccc1970..5147fd46560 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json @@ -1434,6 +1434,7 @@ "It_works": "Äœi funkcias", "italics": "kursivoj", "Job_Title": "Labortitolo", + "Join": "AliÄi", "Join_audio_call": "AliÄi al aÅd-alvoko", "Join_Chat": "AliÄi Babilejon", "Join_default_channels": "AliÄi alfaÅltaj kanaloj", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 47519db384c..9e66920974a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -2371,6 +2371,7 @@ "italics": "cursiva", "Items_per_page:": "Elementos por página:", "Job_Title": "Cargo", + "Join": "Unirse", "Join_audio_call": "Unirse a la llamada de audio", "Join_call": "Unirse a la llamada", "Join_Chat": "Unirse al chat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json index 40d973954d9..44a0bff63ba 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -1697,6 +1697,7 @@ "It_works": "کار Ù…ÛŒ کند", "italics": "کج (ایتالیک)", "Job_Title": "عنوان شغلی", + "Join": "پیوستن", "Join_audio_call": "پیوستن به تماس صوتی", "Join_Chat": "عضویت در چت", "Join_default_channels": "پیوستن به کانال های پیشÙرض", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index dfd27626755..93bbece60ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -1435,6 +1435,7 @@ "It_works": "Se toimii", "italics": "kursivoitu", "Job_Title": "Työnimike", + "Join": "Liity", "Join_audio_call": "Liity äänipuheluun", "Join_Chat": "Liity Chatiin", "Join_default_channels": "Liity oletuskanaviin", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 052d0226be7..a3933314a12 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -2380,6 +2380,7 @@ "italics": "italique", "Items_per_page:": "Éléments par page :", "Job_Title": "Titre d'emploi", + "Join": "Rejoindre", "Join_audio_call": "Rejoindre l'appel audio", "Join_call": "Rejoindre l'appel", "Join_Chat": "Rejoindre le chat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json index 779f3de5995..6cbfff570a8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/he.i18n.json @@ -737,6 +737,7 @@ "is_typing": "מקליד/×”", "It_works": "×–×” עובד", "italics": "× ×•×˜×”", + "Join": "הצטרפות", "Join_audio_call": "הצטרף לשיחת ×ודיו", "Join_Chat": "הצטרף לשיחה", "Join_default_channels": "הצטרפות לערוצי בררת המחדל", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json index 98965109b1d..8a9e5bf662a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -1566,6 +1566,7 @@ "It_works": "Radi", "italics": "ukosi", "Job_Title": "Naziv posla", + "Join": "Pridruži se", "Join_audio_call": "Pridružite se audiopozivu", "Join_Chat": "Pridružite se Chatu", "Join_default_channels": "Pridružite se zadanim kanalima", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 87476b6e0d5..4bc2d9f70ef 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -2015,6 +2015,7 @@ "italic": "dÅ‘lt", "italics": "dÅ‘lt", "Job_Title": "Munka megnevezése", + "Join": "Csatlakozás", "Join_audio_call": "Csatlakozás audio hÃvást", "Join_call": "Csatlakozz a hÃváshoz", "Join_Chat": "Csatlakozás beszélgetéshez", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json index a7df1bc4e38..df85972d997 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json @@ -1433,6 +1433,7 @@ "It_works": "Berhasil", "italics": "italics", "Job_Title": "Judul pekerjaan", + "Join": "Gabung", "Join_audio_call": "Bergabung panggilan suara", "Join_Chat": "Bergabunglah dengan Chat", "Join_default_channels": "Bergabung dengan saluran standar", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index 6aa36396317..2655e36530c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -1483,6 +1483,7 @@ "It_works": "Funziona", "italics": "corsivo", "Job_Title": "Titolo di lavoro", + "Join": "Entra", "Join_audio_call": "Partecipa alla chiamata audio", "Join_Chat": "Iscriviti alla chat", "Join_default_channels": "Partecipa ai canali predefiniti", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 95662c1ad88..fe60c65fefd 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -2372,6 +2372,7 @@ "italics": "斜体", "Items_per_page:": "ページã‚ãŸã‚Šã®ã‚¢ã‚¤ãƒ†ãƒ :", "Job_Title": "å½¹è·", + "Join": "å‚åŠ ", "Join_audio_call": "音声通話ã«å‚åŠ ", "Join_call": "通話ã«å‚åŠ ", "Join_Chat": "ãƒãƒ£ãƒƒãƒˆã«å‚åŠ ", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 89eae1880bf..a6472adcd94 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -1926,6 +1926,7 @@ "italics": "იტáƒáƒšáƒ˜áƒ™áƒ”ბი", "Items_per_page:": "პუბქტები გვერდზე:", "Job_Title": "თáƒáƒœáƒáƒ›áƒ“ებáƒáƒ‘áƒ", + "Join": "შეერთებáƒ", "Join_audio_call": "შეუერთდით áƒáƒ£áƒ“ირზáƒáƒ ს", "Join_Chat": "შეუერთდით ჩáƒáƒ¢áƒ¡", "Join_default_channels": "შეუერთდით დეფáƒáƒ£áƒšáƒ¢ áƒáƒ ხებს", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index d229153e6bb..4b5b0c88b42 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1706,6 +1706,7 @@ "italic": "ទ្រáŸáž", "italics": "ទ្រáŸáž", "Job_Title": "ចំណងជើងការងារ", + "Join": "ចូល​រួម", "Join_audio_call": "ចូលរួមជាមួយការហៅជាសំឡáŸáž„", "Join_Chat": "ចូលរួមជជែក", "Join_default_channels": "ចូលរួមជាមួយបណ្ážáž¶áž‰áž›áŸ†áž“ាំដើម", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 7b80b572658..226b3990979 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -2087,6 +2087,7 @@ "italics": "기울임체", "Items_per_page:": "페ì´ì§€ 당 í•ëª©:", "Job_Title": "ì§ìœ„", + "Join": "참여", "Join_audio_call": "ìŒì„± í†µí™”ì— ì°¸ì—¬", "Join_Chat": "ì±„íŒ…ì— ì°¸ì—¬", "Join_default_channels": "기본 ì±„ë„ ì°¸ì—¬", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json index 2a136c2b27e..5bb87b60984 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -1429,6 +1429,7 @@ "It_works": "Ev kar dike", "italics": "fer-", "Job_Title": "ManÅŸeta ÅŸolê", + "Join": "Bihevgirêdan", "Join_audio_call": "Join deng", "Join_Chat": "Chat Chat", "Join_default_channels": "Join kanalên default", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json index af6a087c02d..2017a1ac65b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -1469,6 +1469,7 @@ "It_works": "ມັນ​ໃຊ້​ໄດ້", "italics": "italics", "Job_Title": "ຕà»àº²â€‹à»àº«àº™à»ˆàº‡", + "Join": "ເຂົ້າຮ່ວມ", "Join_audio_call": "ເຂົ້າຮ່ວມàºàº²àº™à»‚ທສຽງ", "Join_Chat": "ເຂົ້າຮ່ວມສົນທະນາ", "Join_default_channels": "ເຂົ້າຮ່ວມຊ່àºàº‡à»ƒàº™àº•àºàº™àº•àº»à»‰àº™", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json index 5098d693478..b39113c81f9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json @@ -1489,6 +1489,7 @@ "It_works": "Tai veikia", "italics": "kursyvu", "Job_Title": "Darbo pavadinimas", + "Join": "prisijungti", "Join_audio_call": "Prisijungti prie garso skambuÄio", "Join_Chat": "Prisijungti prie pokalbio", "Join_default_channels": "Prisijunk prie numatytųjų kanalų", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json index 7793fa303be..d86a496e24c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json @@ -1447,6 +1447,7 @@ "It_works": "Tas strÄdÄ", "italics": "kursÄ«vs", "Job_Title": "Darba nosaukums", + "Join": "Pievienoties", "Join_audio_call": "Pievienoties audio zvanam", "Join_Chat": "Pievienoties tÄ“rzÄ“Å¡anai", "Join_default_channels": "Pievienoties noklusÄ“juma kanÄliem", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json index 45c4accc3bb..f635fe00420 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -1429,6 +1429,7 @@ "It_works": "ÐÐ½Ñ Ð½ÑŒ ажилладаг", "italics": "налуу", "Job_Title": "Ðлбан тушаал", + "Join": "ÐÑгдÑÑ…", "Join_audio_call": "Ðудио дуудлага хийх", "Join_Chat": "Чатыг нÑгтгÑÑ…", "Join_default_channels": "Ðнхдагч Ñувгууд руу нÑгдÑÑ…", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index 83ed3aca5ef..9a3e2ecc4c3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1431,6 +1431,7 @@ "It_works": "Ianya berfungsi", "italics": "condong", "Job_Title": "Tajuk Pekerjaan", + "Join": "Sertai", "Join_audio_call": "Sertai panggilan audio", "Join_Chat": "Sertai Chat", "Join_default_channels": "Sertai saluran lalai", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 566acf7d749..b46e1ec23b4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -2380,6 +2380,7 @@ "italics": "cursief", "Items_per_page:": "Items per pagina:", "Job_Title": "Functietitel", + "Join": "Toetreden", "Join_audio_call": "Deelnemen aan audiogesprek", "Join_call": "Deelnemen aan gesprek", "Join_Chat": "Met chat meedoen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json index f2198f03712..9702a44f9db 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1507,6 +1507,7 @@ "It_works": "Det fungerer", "italics": "kursiv", "Job_Title": "Jobbtittel", + "Join": "Bli med", "Join_audio_call": "Bli med pÃ¥ lydanrop", "Join_Chat": "Bli med pÃ¥ Chat", "Join_default_channels": "Bli med i standardkanaler", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 29c947f3aaf..7b5db76fa10 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -741,7 +741,6 @@ "Build_Environment": "Åšrodowisko kompilacji", "bulk-register-user": "Utwórz masowo użytkowników", "bulk-register-user_description": "Uprawnienie do masowego tworzenia użytkowników", - "bundle_chip_title": "__bundleName__ Paczka", "Bundles": "Pakiety", "Busiest_day": "Najpracowitszy dzieÅ„", "Busiest_time": "Najpracowitszy czas", @@ -2563,6 +2562,7 @@ "italics": "kursywa", "Items_per_page:": "Elementów na stronÄ™:", "Job_Title": "Stanowisko", + "Join": "DoÅ‚Ä…cz", "Join_audio_call": "DoÅ‚Ä…cz do rozmowy audio", "Join_call": "DoÅ‚Ä…cz do poÅ‚Ä…czenia", "Join_Chat": "DoÅ‚Ä…cz do czatu", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index e39222b4acf..242982fad6e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2385,6 +2385,7 @@ "italics": "itálico", "Items_per_page:": "Itens por página:", "Job_Title": "Cargo", + "Join": "Entrar", "Join_audio_call": "Entrar na chamada de áudio", "Join_call": "Ingressar na chamada", "Join_Chat": "Junte-se à conversa", @@ -4967,4 +4968,4 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponÃveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "É necessário integrar manualmente com serviços externos" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 7875107b8ab..1e3683f9da5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1699,6 +1699,7 @@ "italic": "Itálico", "italics": "itálico", "Job_Title": "Titulo do trabalho", + "Join": "Entrar", "Join_audio_call": "Entrar na chamada de áudio", "Join_Chat": "Entrar no Chat", "Join_default_channels": "Entrar em canais predefinidos", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json index 5744d1fd5bd..6d947de2d27 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -1433,6 +1433,7 @@ "It_works": "Functioneaza", "italics": "cursive", "Job_Title": "Denumirea funcÈ›iei", + "Join": "AlăturaÈ›i-vă", "Join_audio_call": "Intră în apel audio", "Join_Chat": "AlăturaÈ›i-vă Chat", "Join_default_channels": "AlăturaÈ›i-vă canale implicite", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 10c6394f88e..2da1a671e93 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -2397,6 +2397,7 @@ "italics": "курÑив", "Items_per_page:": "Ðлементов на Ñтраницу:", "Job_Title": "ДолжноÑÑ‚ÑŒ", + "Join": "ПриÑоединитьÑÑ", "Join_audio_call": "ПриÑоединитьÑÑ Ðº аудиозвонку", "Join_call": "ПриÑоединитьÑÑ Ðº вызову", "Join_Chat": "ПриÑоединитьÑÑ Ðº чату", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json index 1ff93c913b3..e3e0bec459e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json @@ -1444,6 +1444,7 @@ "It_works": "Funguje to", "italics": "kurzÃva", "Job_Title": "Názov práce", + "Join": "pripojiÅ¥", "Join_audio_call": "Pripojte zvukový hovor", "Join_Chat": "Pripojte sa k rozhovoru", "Join_default_channels": "PripojiÅ¥ predvolené kanály", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json index 5b67c5848ec..0b2a560b177 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json @@ -1424,6 +1424,7 @@ "It_works": "Deluje", "italics": "poÅ¡evno", "Job_Title": "Naziv delovnega mesta", + "Join": "Pridruži se", "Join_audio_call": "Pridruži se avdio klicu", "Join_Chat": "Pridružite se klepetu", "Join_default_channels": "Pridružite se privzetim kanalom", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json index 3d35601cf37..9ae5373a440 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -1433,6 +1433,7 @@ "It_works": "Punon", "italics": "italics", "Job_Title": "Titulli i Punës", + "Join": "Bashkohuni", "Join_audio_call": "Join thirrje audio", "Join_Chat": "Bashkohu me Chat", "Join_default_channels": "Join kanale parazgjedhur", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json index 4a73f3a7cad..f8600df6ff9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -1273,6 +1273,7 @@ "It_works": "То ради", "italics": "курзив", "Job_Title": "Звање", + "Join": "Придружити", "Join_audio_call": "Придружи Ñе аудио позиву", "Join_Chat": "Придружите Ñе ћаÑкању", "Join_default_channels": "Придружите дефаулт канала", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 2c54a7dc038..5acd2f322d1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -1507,6 +1507,7 @@ "It_works": "Det fungerar", "italics": "kursiv", "Job_Title": "Jobbtitel", + "Join": "GÃ¥ med", "Join_audio_call": "GÃ¥ med i ljudsamtal", "Join_Chat": "GÃ¥ med i chatt", "Join_default_channels": "GÃ¥ med standardkanaler", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 3edd025013a..b20409b768f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -1433,6 +1433,7 @@ "It_works": "இத௠எபà¯à®ªà®Ÿà®¿ வேலை செயà¯à®•à®¿à®±à®¤à¯", "italics": "சாயà¯à®µà¯", "Job_Title": "வேலை தலைபà¯à®ªà¯", + "Join": "சேர", "Join_audio_call": "ஆடியோ அழைபà¯à®ªà¯ சேர", "Join_Chat": "சேட௠சேரவà¯à®®à¯", "Join_default_channels": "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ சேனலà¯à®•à®³à¯ சேர", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json index 75e8a081275..5463563e618 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json @@ -1428,6 +1428,7 @@ "It_works": "มันได้ผล", "italics": "หนังสืà¸à¹à¸šà¸šà¸•à¸±à¸§à¹€à¸à¸µà¸¢à¸‡", "Job_Title": "ตำà¹à¸«à¸™à¹ˆà¸‡à¸‡à¸²à¸™", + "Join": "ร่วม", "Join_audio_call": "เข้าร่วมà¸à¸²à¸£à¹‚ทรด้วยเสียง", "Join_Chat": "เข้าร่วมà¹à¸Šà¸—", "Join_default_channels": "เข้าร่วมช่à¸à¸‡à¹€à¸£à¸´à¹ˆà¸¡à¸•à¹‰à¸™", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index eb197123bf4..055d7de9261 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -1724,6 +1724,7 @@ "italic": "Ä°talik", "italics": "EÄŸik", "Job_Title": "Ä°ÅŸ ismi", + "Join": "Katıl", "Join_audio_call": "Sesli görüşmeye katıl", "Join_Chat": "Sohbete Katıl", "Join_default_channels": "Varsayılan kanala katıl", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json index aaa96194183..5fc930a7bca 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ug.i18n.json @@ -557,6 +557,7 @@ "is_typing": "كىرگۈزۈلۈۋاتىدۇ", "It_works": "ئۇتۇقلۇق بولدى", "italics": "يانتۇ خەت نۇسخىسى", + "Join": "قوشۇلۇش", "Join_audio_call": "ئاۋازلىق سۆھبەتكە قوشۇلۇش", "Join_default_channels": "بەلگىلەنگەن قانالغا قوشۇلۇش", "Join_the_Community": "ئىجتىمائىي رايونغا قوشۇلۇش", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json index 8e71f223768..d1d43bae674 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -1885,6 +1885,7 @@ "italic": "КурÑив", "italics": "курÑив", "Job_Title": "ПрофеÑійний титул", + "Join": "приєднатиÑÑ", "Join_audio_call": "РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ Ð°ÑƒÐ´Ñ–Ð¾ виклику", "Join_Chat": "ПриєднатиÑÑ Ð´Ð¾ чату", "Join_default_channels": "РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐºÐ°Ð½Ð°Ð»Ñ–Ð² за замовчуваннÑм", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json index 592aa1489d5..33efc075367 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json @@ -1527,6 +1527,7 @@ "It_works": "Nó hoạt Ä‘á»™ng", "italics": "chữ in nghiêng", "Job_Title": "Chức vụ nghá» nghiệp", + "Join": "Tham gia", "Join_audio_call": "Tham gia cuá»™c gá»i âm thanh", "Join_Chat": "Tham gia Trò chuyện", "Join_default_channels": "Tham gia các kênh mặc định", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index 038d177c35b..77193c8eeb3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -1453,6 +1453,7 @@ "It_works": "有用", "italics": "斜体", "Job_Title": "èŒç§°", + "Join": "åŠ å…¥", "Join_audio_call": "åŠ å…¥éŸ³é¢‘é€šè¯", "Join_Chat": "åŠ å…¥èŠå¤©", "Join_default_channels": "åŠ å…¥é»˜è®¤é¢‘é“", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index a58c4aebe8b..f6e0e96a114 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -2317,6 +2317,7 @@ "italics": "斜體", "Items_per_page:": "æ¯é é …ç›®:", "Job_Title": "è·ç¨±", + "Join": "åŠ å…¥", "Join_audio_call": "åŠ å…¥éŸ³è¨Šå‘¼å«", "Join_call": "åŠ å…¥é€šè©±", "Join_Chat": "åŠ å…¥èŠå¤©", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index fbfd77b2983..0d8fc4a2ff7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -2123,6 +2123,7 @@ "italics": "斜体", "Items_per_page:": "æ¯é¡µæ¡æ•°ï¼š", "Job_Title": "èŒç§°", + "Join": "åŠ å…¥", "Join_audio_call": "åŠ å…¥éŸ³é¢‘å¯¹è¯", "Join_Chat": "åŠ å…¥èŠå¤©", "Join_default_channels": "åŠ å…¥é»˜è®¤é¢‘é“", -- GitLab From aef67a2a53361b9d5faf1ce11b31c00392d5f160 Mon Sep 17 00:00:00 2001 From: Martin Schoeler <martin.schoeler@rocket.chat> Date: Wed, 21 Sep 2022 15:54:46 -0300 Subject: [PATCH 050/107] [IMPROVE] Better descriptions for VoIP Settings (#26877) --- .../meteor/app/lib/server/startup/settings.ts | 44 ++++++++++++------- .../rocketchat-i18n/i18n/en.i18n.json | 20 +++++---- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 15 ++++--- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index cb2820da3ac..7c60d8c9c02 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -3216,27 +3216,32 @@ settingsRegistry.addGroup('Troubleshoot', function () { }); settingsRegistry.addGroup('Call_Center', function () { + // TODO: Check with the backend team if an i18nPlaceholder is possible this.with({ tab: 'Settings' }, function () { - this.add('VoIP_Enabled', false, { - type: 'boolean', - public: true, - enableQuery: { - _id: 'Livechat_enabled', - value: true, - }, - }); - this.add('VoIP_JWT_Secret', '', { - type: 'password', - i18nDescription: 'VoIP_JWT_Secret_description', - enableQuery: { - _id: 'VoIP_Enabled', - value: true, - }, + this.section('General_Settings', function () { + this.add('VoIP_Enabled', false, { + type: 'boolean', + public: true, + i18nDescription: 'VoIP_Enabled_Description', + enableQuery: { + _id: 'Livechat_enabled', + value: true, + }, + }); + this.add('VoIP_JWT_Secret', '', { + type: 'password', + i18nDescription: 'VoIP_JWT_Secret_description', + enableQuery: { + _id: 'VoIP_Enabled', + value: true, + }, + }); }); - this.section('Server_Configuration', function () { + this.section('Voip_Server_Configuration', function () { this.add('VoIP_Server_Name', '', { type: 'string', public: true, + placeholder: 'WebSocket Server', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3245,6 +3250,7 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Server_Websocket_Path', '', { type: 'string', public: true, + placeholder: 'wss://your.domain.name', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3253,6 +3259,8 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Retry_Count', -1, { type: 'int', public: true, + i18nDescription: 'VoIP_Retry_Count_Description', + placeholder: '1', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3273,6 +3281,7 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Management_Server_Host', '', { type: 'string', public: true, + placeholder: 'https://your.domain.name', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3282,6 +3291,7 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Management_Server_Port', 0, { type: 'int', public: true, + placeholder: '8080', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3291,6 +3301,7 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Management_Server_Name', '', { type: 'string', public: true, + placeholder: 'Server Name', enableQuery: { _id: 'VoIP_Enabled', value: true, @@ -3300,6 +3311,7 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Management_Server_Username', '', { type: 'string', public: true, + placeholder: 'Username', enableQuery: { _id: 'VoIP_Enabled', value: true, diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 4356bb33902..0de9a8ff0cb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -764,8 +764,8 @@ "cache_cleared": "Cache cleared", "Call": "Call", "Calling": "Calling", - "Call_Center": "Call Center", - "Call_Center_Description": "Configure Rocket.Chat call center.", + "Call_Center": "Voice Channel", + "Call_Center_Description": "Configure Rocket.Chat's voice channels", "Call_ended": "Call ended", "Calls": "Calls", "Calls_in_queue": "__calls__ call in queue", @@ -2247,6 +2247,7 @@ "Gaming": "Gaming", "General": "General", "General_Description": "Configure general workspace settings.", + "General_Settings": "General Settings", "Generate_new_key": "Generate a new key", "Generate_New_Link": "Generate New Link", "Generating_key": "Generating key", @@ -3067,7 +3068,7 @@ "Manager_removed": "Manager removed", "Managers": "Managers", "manage-chatpal": "Manage Chatpal", - "Management_Server": "Management Server", + "Management_Server": "Asterisk Manager Interface (AMI)", "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", "Manual_Selection": "Manual Selection", @@ -5180,11 +5181,12 @@ "Visitor_page_URL": "Visitor page URL", "Visitor_time_on_site": "Visitor time on site", "Voice_Call": "Voice Call", - "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "Enable Keep-Alive using SIP-OPTIONS for unstable networks", - "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "Enables or Disables Keep-Alive using SIP-OPTIONS based on network quality", - "VoIP_Enabled": "VoIP Enabled", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "Enable SIP Options Keep Alive", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "Monitor the status of multiple external SIP gateways by sending periodic SIP OPTIONS messages. Used for unstable networks.", + "VoIP_Enabled": "Enable voice channel", + "VoIP_Enabled_Description": "Connect agents to customers through outbound and incoming calls", "VoIP_Extension": "VoIP Extension", - "Voip_Server_Configuration": "Server Configuration", + "Voip_Server_Configuration": "Asterisk WebSocket Server", "VoIP_Server_Websocket_Port": "Websocket Port", "VoIP_Server_Name": "Server Name", "VoIP_Server_Websocket_Path": "Websocket URL", @@ -5204,8 +5206,8 @@ "Voip_call_ended": "Call ended at", "Voip_call_ended_unexpectedly": "Call ended unexpectedly: __reason__", "Voip_call_wrapup": "Call wrapup notes added: __comment__", - "VoIP_JWT_Secret": "VoIP JWT Secret", - "VoIP_JWT_Secret_description": "This allows you to set a secret key for sharing extension details from server to client as JWT instead of plain text. If you don't setup this, extension registration details will be sent as plain text", + "VoIP_JWT_Secret": "Secret key (JWT)", + "VoIP_JWT_Secret_description": "Set a secret key for sharing extension details from server to client as JWT instead of plain text. Extension registration details will be sent as plain text if a secret key has not been set.", "Voip_is_disabled": "VoIP is disabled", "Voip_is_disabled_description": "To view the list of extensions it is necessary to activate VoIP, do so in the Settings tab.", "VoIP_Toggle": "Enable/Disable VoIP", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 242982fad6e..6d76ed6eafe 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -705,7 +705,8 @@ "cache_cleared": "Cache limpo", "Call": "Ligação", "Calling": "Chamando", - "Call_Center": "Central de atendimento", + "Call_Center": "Canal de Voz", + "Call_Center_Description": "Configure o canal de voz no Rocket.Chat", "Calls_in_queue": "__calls__ chamadas na fila", "Calls_in_queue_plural": "__calls__ Chamadas na Fila", "Calls_in_queue_empty": "A fila está Vazia", @@ -2078,6 +2079,7 @@ "Full_Screen": "Tela cheia", "Gaming": "Jogos", "General": "Geral", + "General_Settings": "Configurações Gerais", "Generate_new_key": "Gerar uma nova chave", "Generate_New_Link": "Gerar novo link", "Generating_key": "Gerando chave", @@ -2838,7 +2840,7 @@ "Manager_removed": "Gerente removido", "Managers": "Gerentes", "manage-chatpal": "Gerenciar Chatpal", - "Management_Server": "Servidor de gerenciamento", + "Management_Server": "Interface de gerenciamento Asterisk (AMI)", "Managing_assets": "Gerenciando recursos", "Managing_integrations": "Gerenciando integrações", "Manual_Selection": "Seleção manual", @@ -4733,9 +4735,12 @@ "Visitor_page_URL": "URL da página do visitante", "Visitor_time_on_site": "Tempo do visitante no site", "Voice_Call": "Chamada de voz", - "VoIP_Enabled": "VoIP habilitado", + "VoIP_Enabled": "Canal de voz habilitado", + "VoIP_Enabled_Description": "Conecte agentes e clientes através de chamadas de voz", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "SIP Options Keep Alive habilitado", + "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "Monitore o status de múltiplos gateways SIP externos enviando mensagens SIP OPTIONS periódicas. Usado para redes instáveis.", "VoIP_Extension": "Extensão de VoIP", - "Voip_Server_Configuration": "Configuração do servidor", + "Voip_Server_Configuration": "Servidor Websocket Asterisk", "VoIP_Server_Websocket_Port": "Porta do webSocket", "VoIP_Server_Name": "Nome do servidor", "VoIP_Server_Websocket_Path": "URL do webSocket", @@ -4753,7 +4758,7 @@ "Voip_call_ended": "Chamada encerrada à s", "Voip_call_ended_unexpectedly": "Chamada encerrada de forma inesperada: __reason__", "Voip_call_wrapup": "Adição de notas de encerramento da chamada: __comment__", - "VoIP_JWT_Secret": "VoIP JWT Secret", + "VoIP_JWT_Secret": "Chave Secreta JWT", "VoIP_JWT_Secret_description": "Isso permite que você defina uma chave secreta para compartilhar detalhes da extensão do servidor para o cliente como JWT, em vez de texto simples. Se você não configurar isso, os detalhes do registro da extensão serão enviados como texto simples.", "Voip_is_disabled": "VoIP está desabilitado", "Voip_is_disabled_description": "Para ver a lista de extensões é necessário ativar o VoIP, faça isso na aba Configurações.", -- GitLab From 5c26dd9636e82640884dedc613f119af912975c3 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty <debdut.chakraborty@rocket.chat> Date: Thu, 22 Sep 2022 01:52:48 +0530 Subject: [PATCH 051/107] [NEW] Get moderators, owners and leaders from room scope via apps-engine (#26674) --- apps/meteor/app/apps/server/bridges/rooms.ts | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 401b0459970..591a8b9465b 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -4,7 +4,7 @@ import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { Meteor } from 'meteor/meteor'; -import type { ISubscription } from '@rocket.chat/core-typings'; +import type { ISubscription, IUser as ICoreUser } from '@rocket.chat/core-typings'; import type { AppServerOrchestrator } from '../orchestrator'; import { Rooms, Subscriptions, Users } from '../../../models/server'; @@ -169,4 +169,26 @@ export class AppRoomBridge extends RoomBridge { return rid; } + + protected getModerators(roomId: string, appId: string): Promise<IUser[]> { + this.orch.debugLog(`The App ${appId} is getting room moderators for room id: ${roomId}`); + return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'moderator'); + } + + protected getOwners(roomId: string, appId: string): Promise<IUser[]> { + this.orch.debugLog(`The App ${appId} is getting room owners for room id: ${roomId}`); + return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'owner'); + } + + protected getLeaders(roomId: string, appId: string): Promise<IUser[]> { + this.orch.debugLog(`The App ${appId} is getting room leaders for room id: ${roomId}`); + return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'leader'); + } + + private async getUsersByRoomIdAndSubscriptionRole(roomId: string, role: string): Promise<IUser[]> { + const subs = await Subscriptions.findByRoomIdAndRoles(roomId, [role], { projection: { uid: '$u._id', _id: 0 } }); + const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)); + const userConverter = this.orch.getConverters()!.get('users'); + return users.map((user: ICoreUser) => userConverter!.convertToApp(user)); + } } -- GitLab From 414d3e3f795bee32cb95b05c45bd0e5706da5f3e Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Thu, 22 Sep 2022 03:41:05 +0530 Subject: [PATCH 052/107] [IMPROVE] Setting for login email notifications (#26831) --- .../PreferencesNotificationsSection.tsx | 38 +++++++++++++++++-- .../AccountTokensTable/AccountTokensTable.tsx | 2 +- .../DeviceManagementTable.tsx | 2 +- .../DeviceManagementInfoWithData.tsx | 2 +- .../EngagementDashboardCardErrorBoundary.tsx | 2 +- .../ee/server/lib/deviceManagement/session.ts | 23 +++++++---- .../ee/server/settings/deviceManagement.ts | 26 +++++++++++++ .../ee/server/startup/deviceManagement.ts | 2 + .../rocketchat-i18n/i18n/de.i18n.json | 3 +- .../rocketchat-i18n/i18n/en.i18n.json | 34 ++++++++++------- .../rocketchat-i18n/i18n/pl.i18n.json | 3 +- .../v1/users/UsersSetPreferenceParamsPOST.ts | 1 + .../methods/saveUserPreferences.ts | 1 + 13 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 apps/meteor/ee/server/settings/deviceManagement.ts diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index e042d090873..2fcc2ff72df 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -26,6 +26,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form const userDesktopNotifications = useUserPreference('desktopNotifications'); const userMobileNotifications = useUserPreference('pushNotifications'); const userEmailNotificationMode = useUserPreference('emailNotificationMode') as keyof typeof emailNotificationOptionsLabelMap; + const userReceiveLoginDetectionEmail = useUserPreference('receiveLoginDetectionEmail'); const defaultDesktopNotifications = useSetting( 'Accounts_Default_User_Preferences_desktopNotifications', @@ -35,25 +36,42 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form ) as keyof typeof notificationOptionsLabelMap; const canChangeEmailNotification = useSetting('Accounts_AllowEmailNotifications'); + const loginEmailEnabled = useSetting('Device_Management_Enable_Login_Emails'); + const allowLoginEmailPreference = useSetting('Device_Management_Allow_Login_Email_preference'); + const showNewLoginEmailPreference = loginEmailEnabled && allowLoginEmailPreference; + const { values, handlers, commit } = useForm( { desktopNotificationRequireInteraction: userDesktopNotificationRequireInteraction, desktopNotifications: userDesktopNotifications, pushNotifications: userMobileNotifications, emailNotificationMode: userEmailNotificationMode, + receiveLoginDetectionEmail: userReceiveLoginDetectionEmail, }, onChange, ); - const { desktopNotificationRequireInteraction, desktopNotifications, pushNotifications, emailNotificationMode } = values as { + const { + desktopNotificationRequireInteraction, + desktopNotifications, + pushNotifications, + emailNotificationMode, + receiveLoginDetectionEmail, + } = values as { desktopNotificationRequireInteraction: boolean; desktopNotifications: string | number | readonly string[]; pushNotifications: string | number | readonly string[]; emailNotificationMode: string; + receiveLoginDetectionEmail: boolean; }; - const { handleDesktopNotificationRequireInteraction, handleDesktopNotifications, handlePushNotifications, handleEmailNotificationMode } = - handlers; + const { + handleDesktopNotificationRequireInteraction, + handleDesktopNotifications, + handlePushNotifications, + handleEmailNotificationMode, + handleReceiveLoginDetectionEmail, + } = handlers; useEffect(() => setNotificationsPermission(window.Notification && Notification.permission), []); @@ -118,7 +136,7 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form </Field.Row> </Field> <Field> - <Box display='flex' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <Field.Label>{t('Notification_RequireInteraction')}</Field.Label> <Field.Row> <ToggleSwitch checked={desktopNotificationRequireInteraction} onChange={handleDesktopNotificationRequireInteraction} /> @@ -153,6 +171,18 @@ const PreferencesNotificationsSection = ({ onChange, commitRef, ...props }: Form {!canChangeEmailNotification && t('Email_Notifications_Change_Disabled')} </Field.Hint> </Field> + + {showNewLoginEmailPreference && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <Field.Label>{t('Receive_Login_Detection_Emails')}</Field.Label> + <Field.Row> + <ToggleSwitch checked={receiveLoginDetectionEmail} onChange={handleReceiveLoginDetectionEmail} /> + </Field.Row> + </Box> + <Field.Hint>{t('Receive_Login_Detection_Emails_Description')}</Field.Hint> + </Field> + )} </FieldGroup> </Accordion.Item> ); diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx index 69d4e1b24c0..65ce11ca5bb 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx @@ -123,7 +123,7 @@ const AccountTokensTable = (): ReactElement => { <Box display='flex' justifyContent='center' alignItems='center' height='100%'> <States> <StatesIcon name='warning' variation='danger' /> - <StatesTitle>{t('Something_Went_Wrong')}</StatesTitle> + <StatesTitle>{t('Something_went_wrong')}</StatesTitle> <StatesSubtitle>{t('We_Could_not_retrive_any_data')}</StatesSubtitle> <StatesSubtitle>{error?.message}</StatesSubtitle> <StatesActions> diff --git a/apps/meteor/ee/client/deviceManagement/components/DeviceManagementTable/DeviceManagementTable.tsx b/apps/meteor/ee/client/deviceManagement/components/DeviceManagementTable/DeviceManagementTable.tsx index 3bccecb2e3c..8ddd890c5f0 100644 --- a/apps/meteor/ee/client/deviceManagement/components/DeviceManagementTable/DeviceManagementTable.tsx +++ b/apps/meteor/ee/client/deviceManagement/components/DeviceManagementTable/DeviceManagementTable.tsx @@ -46,7 +46,7 @@ const DeviceManagementTable = <T extends DeviceManagementSession | DeviceManagem <Box display='flex' justifyContent='center' alignItems='center' height='100%'> <States> <StatesIcon name='warning' variation='danger' /> - <StatesTitle>{t('Something_Went_Wrong')}</StatesTitle> + <StatesTitle>{t('Something_went_wrong')}</StatesTitle> <StatesSubtitle>{t('We_Could_not_retrive_any_data')}</StatesSubtitle> <StatesSubtitle>{error?.message}</StatesSubtitle> <StatesActions> diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx index 131c2bc13e5..3c1a7036a5b 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx +++ b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx @@ -49,7 +49,7 @@ const DeviceInfoWithData = ({ deviceId, onReload }: { deviceId: string; onReload <Box display='flex' justifyContent='center' alignItems='center' height='100%'> <States> <StatesIcon name='warning' variation='danger' /> - <StatesTitle>{t('Something_Went_Wrong')}</StatesTitle> + <StatesTitle>{t('Something_went_wrong')}</StatesTitle> <StatesSubtitle>{t('We_Could_not_retrive_any_data')}</StatesSubtitle> <StatesSubtitle>{error?.message}</StatesSubtitle> </States> diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx index 80236e1487f..2d51ed66af5 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx @@ -29,7 +29,7 @@ const EngagementDashboardCardErrorBoundary = ({ children }: EngagementDashboardC fallbackRender={({ resetErrorBoundary }): ReactElement => ( <States> <StatesIcon name='circle-exclamation' /> - <StatesTitle>{t('Something_Went_Wrong')}</StatesTitle> + <StatesTitle>{t('Something_went_wrong')}</StatesTitle> <StatesSubtitle>{isError(error) && error?.message}</StatesSubtitle> <StatesActions data-qa='EngagementDashboardCardErrorBoundary'> <StatesAction onClick={(): void => resetErrorBoundary()}>{t('Retry')}</StatesAction> diff --git a/apps/meteor/ee/server/lib/deviceManagement/session.ts b/apps/meteor/ee/server/lib/deviceManagement/session.ts index 8396ad4ba5d..cfd3488bce1 100644 --- a/apps/meteor/ee/server/lib/deviceManagement/session.ts +++ b/apps/meteor/ee/server/lib/deviceManagement/session.ts @@ -9,7 +9,7 @@ import { settings } from '../../../../app/settings/server'; import { UAParserDesktop, UAParserMobile } from '../../../../app/statistics/server/lib/UAParserCustom'; import { deviceManagementEvents } from '../../../../server/services/device-management/events'; import { hasLicense } from '../../../app/license/server/license'; -import { t } from '../../../../app/utils/server'; +import { t, getUserPreference } from '../../../../app/utils/server'; let mailTemplates: string; @@ -84,13 +84,20 @@ export const listenSessionLogin = async (): Promise<void> => { } try { - Mailer.send({ - to: `${name} <${email}>`, - from: Accounts.emailTemplates.from, - subject: settings.get('Device_Management_Email_Subject'), - html: mailTemplates, - data: mailData, - }); + const userReceiveLoginEmailPreference = settings.get('Device_Management_Allow_Login_Email_preference') + ? getUserPreference(userId, 'receiveLoginDetectionEmail', true) + : true; + const shouldSendLoginEmail = settings.get('Device_Management_Enable_Login_Emails') && userReceiveLoginEmailPreference; + + if (shouldSendLoginEmail) { + Mailer.send({ + to: `${name} <${email}>`, + from: Accounts.emailTemplates.from, + subject: settings.get('Device_Management_Email_Subject'), + html: mailTemplates, + data: mailData, + }); + } } catch ({ message }) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { method: 'listenSessionLogin', diff --git a/apps/meteor/ee/server/settings/deviceManagement.ts b/apps/meteor/ee/server/settings/deviceManagement.ts new file mode 100644 index 00000000000..7dcdb53a285 --- /dev/null +++ b/apps/meteor/ee/server/settings/deviceManagement.ts @@ -0,0 +1,26 @@ +import { settingsRegistry } from '../../../app/settings/server'; + +export function addSettings(): void { + settingsRegistry.addGroup('Device_Management', function () { + this.with( + { + enterprise: true, + modules: ['device-management'], + }, + function () { + this.add('Device_Management_Enable_Login_Emails', true, { + type: 'boolean', + public: true, + invalidValue: true, + }); + + this.add('Device_Management_Allow_Login_Email_preference', true, { + type: 'boolean', + public: true, + invalidValue: true, + enableQuery: { _id: 'Device_Management_Enable_Login_Emails', value: true }, + }); + }, + ); + }); +} diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index 9258201056a..cb1c782ead9 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,10 +1,12 @@ import { onToggledFeature } from '../../app/license/server/license'; +import { addSettings } from '../settings/deviceManagement'; onToggledFeature('device-management', { up: async () => { const { createPermissions, createEmailTemplates } = await import('../lib/deviceManagement/startup'); const { listenSessionLogin } = await import('../lib/deviceManagement/session'); + addSettings(); await createPermissions(); await createEmailTemplates(); await listenSessionLogin(); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 5b5b8bef547..2d145afb85d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -5260,6 +5260,5 @@ "Device_Management_Device_Unknown": "Unbekannt", "Device_Management_IP": "IP", "Device_Management_Email_Subject": "[Site_Name] - Anmeldung erkannt", - "Device_Management_Email_Body": "Sie können die folgenden Platzhalter verwenden: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", - "Something_Went_Wrong": "Etwas ist schief gelaufen" + "Device_Management_Email_Body": "Sie können die folgenden Platzhalter verwenden: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>" } \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0de9a8ff0cb..94a66453c94 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -31,6 +31,7 @@ "Accept_with_no_online_agents": "Accept with No Online Agents", "Access_not_authorized": "Access not authorized", "Access_Token_URL": "Access Token URL", + "Access_Your_Account": "Access Your Account", "access-mailer": "Access Mailer Screen", "access-mailer_description": "Permission to send mass email to all users.", "access-permissions": "Access Permissions Screen", @@ -1531,6 +1532,18 @@ "Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.", "Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)", "Device_Management": "Device management", + "Device_Management_Allow_Login_Email_preference": "Allow workspace members to turn off login detection emails", + "Device_Management_Allow_Login_Email_preference_Description": "Individual members can set their preference. Useful when frequent login expirations are set causing members to login frequently.", + "Device_Management_Client": "Client", + "Device_Management_Description": "Configure security and access control policies.", + "Device_Management_Device": "Device", + "Device_Management_Device_Unknown": "Unknown", + "Device_Management_Email_Subject": "[Site_Name] - Login Detected", + "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", + "Device_Management_Enable_Login_Emails": "Enable login detection emails", + "Device_Management_Enable_Login_Emails_Description": "Emails are sent to workspace members each time new logins are detected on their accounts.", + "Device_Management_IP": "IP", + "Device_Management_OS": "OS", "Device_ID": "Device ID", "Device_Info": "Device Info", "Device_Logged_Out": "Device logged out", @@ -2972,6 +2985,8 @@ "Logged_out_of_other_clients_successfully": "Logged out of other clients successfully", "Login": "Login", "Login_Attempts": "Failed Login Attempts", + "Login_Detected": "Login detected", + "Logged_In_Via": "Logged in via", "Login_Logs": "Login Logs", "Login_Logs_ClientIp": "Show Client IP on failed login attempts logs", "Login_Logs_Enabled": "Log (on console) failed login attempts", @@ -3574,6 +3589,7 @@ "optional": "optional", "Options": "Options", "or": "or", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Or copy and paste this URL into a tab of your browser", "Or_talk_as_anonymous": "Or talk as anonymous", "Order": "Order", "Organization_Email": "Organization Email", @@ -3650,6 +3666,7 @@ "Phone_call": "Phone Call", "Phone_Number": "Phone Number", "Thank_you_exclamation_mark": "Thank you!", + "Thank_You_For_Choosing_RocketChat": "Thank you for choosing Rocket.Chat!", "Phone_already_exists": "Phone already exists", "Phone_number": "Phone number", "PID": "PID", @@ -3824,6 +3841,8 @@ "Receive_alerts": "Receive alerts", "Receive_Group_Mentions": "Receive @all and @here mentions", "Receive_login_notifications": "Receive login notifications", + "Receive_Login_Detection_Emails": "Receive login detection emails", + "Receive_Login_Detection_Emails_Description":"Receive an email each time a new login is detected on your account.", "Recent_Import_History": "Recent Import History", "Record": "Record", "recording": "recording", @@ -5435,18 +5454,5 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", "onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services", - "Login_Detected": "Login detected", - "Logged_In_Via": "Logged in via", - "Access_Your_Account": "Access Your Account", - "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Or copy and paste this URL into a tab of your browser", - "Thank_You_For_Choosing_RocketChat": "Thank you for choosing Rocket.Chat!", - "Toolbox_room_actions": "Primary Room actions", - "Device_Management_Client": "Client", - "Device_Management_OS": "OS", - "Device_Management_Device": "Device", - "Device_Management_Device_Unknown": "Unknown", - "Device_Management_IP": "IP", - "Device_Management_Email_Subject": "[Site_Name] - Login Detected", - "Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", - "Something_Went_Wrong": "Something went wrong" + "Toolbox_room_actions": "Primary Room actions" } \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 7b5db76fa10..1e7f652775f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -5386,6 +5386,5 @@ "Device_Management_Device_Unknown": "Nieznane", "Device_Management_IP": "IP", "Device_Management_Email_Subject": "[Site_Name] - Login wykryty", - "Device_Management_Email_Body": "Możesz użyć nastÄ™pujÄ…cych placeholderów: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", - "Something_Went_Wrong": "CoÅ› poszÅ‚o nie tak" + "Device_Management_Email_Body": "Możesz użyć nastÄ™pujÄ…cych placeholderów: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>" } \ No newline at end of file diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index aedf4594200..df7a2aca411 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -38,6 +38,7 @@ export type UsersSetPreferencesParamsPOST = { sidebarDisplayAvatar?: boolean; sidebarGroupByType?: boolean; muteFocusedConversations?: boolean; + receiveLoginDetectionEmail?: boolean; }; }; diff --git a/packages/ui-contexts/src/ServerContext/methods/saveUserPreferences.ts b/packages/ui-contexts/src/ServerContext/methods/saveUserPreferences.ts index 7e6b20ad0bf..df0dcc7a46a 100644 --- a/packages/ui-contexts/src/ServerContext/methods/saveUserPreferences.ts +++ b/packages/ui-contexts/src/ServerContext/methods/saveUserPreferences.ts @@ -30,6 +30,7 @@ type UserPreferences = { sidebarGroupByType: boolean; muteFocusedConversations: boolean; dontAskAgainList: { action: string; label: string }[]; + receiveLoginDetectionEmail: boolean; }; export type SaveUserPreferencesMethod = (preferences: Partial<UserPreferences>) => boolean; -- GitLab From 6ae770b40da1318378ec5faed73e996e0e607358 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty <debdut.chakraborty@rocket.chat> Date: Thu, 22 Sep 2022 05:02:11 +0530 Subject: [PATCH 053/107] [FIX] Error when mentioning a non-member of a public channel (#26917) --- apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js index 4574788a58c..7ab19152ba7 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js @@ -372,7 +372,7 @@ export async function sendAllNotifications(message, room) { }), ) .then((users) => { - const subscriptions = Subscriptions.findByRoomIdAndUserIds(room._id, users); + const subscriptions = Subscriptions.findByRoomIdAndUserIds(room._id, users).fetch(); users.forEach((userId) => { const subscription = subscriptions.find((subscription) => subscription.u._id === userId); -- GitLab From 3e4b616a9e0a3598f0ac18defa34b65642a981cd Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat> Date: Thu, 22 Sep 2022 00:11:10 -0300 Subject: [PATCH 054/107] [FIX] [Livechat] Unread messages badge (#26843) --- .../tests/e2e/omnichannel-close-chat.spec.ts | 20 +-- .../tests/e2e/omnichannel-livechat.spec.ts | 81 +++++++----- ...channel-transfer-to-another-agents.spec.ts | 40 +++--- .../page-objects/fragments/home-content.ts | 10 +- .../fragments/home-omnichannel-content.ts | 25 ++++ .../tests/e2e/page-objects/fragments/index.ts | 2 + .../fragments/omnichannel-close-chat-modal.ts | 17 +++ .../e2e/page-objects/home-omnichannel.ts | 28 +++++ apps/meteor/tests/e2e/page-objects/index.ts | 1 + .../e2e/page-objects/omnichannel-livechat.ts | 8 +- .../livechat/src/components/Button/index.js | 119 ++++++++++-------- packages/livechat/src/i18n/en.json | 2 + packages/livechat/src/lib/room.js | 5 - .../livechat/src/routes/Chat/container.js | 5 +- 14 files changed, 231 insertions(+), 132 deletions(-) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/home-omnichannel.ts diff --git a/apps/meteor/tests/e2e/omnichannel-close-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel-close-chat.spec.ts index 7c083a7b03e..cd2c0e64bfd 100644 --- a/apps/meteor/tests/e2e/omnichannel-close-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-close-chat.spec.ts @@ -2,22 +2,22 @@ import { faker } from '@faker-js/faker'; import type { Browser, Page } from '@playwright/test'; import { test, expect } from './utils/test'; -import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { OmnichannelLiveChat, HomeOmnichannel } from './page-objects'; -const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeOmnichannel: HomeOmnichannel }> => { const page = await browser.newPage({ storageState }); - const poHomeChannel = new HomeChannel(page); + const poHomeOmnichannel = new HomeOmnichannel(page); await page.goto('/'); await page.locator('.main-content').waitFor(); - return { page, poHomeChannel }; + return { page, poHomeOmnichannel }; }; test.describe('Omnichannel close chat', () => { let poLiveChat: OmnichannelLiveChat; let newUser: { email: string; name: string }; - let agent: { page: Page; poHomeChannel: HomeChannel }; + let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { newUser = { @@ -49,14 +49,14 @@ test.describe('Omnichannel close chat', () => { }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeChannel.sidenav.openChat(newUser.name); + await agent.poHomeOmnichannel.sidenav.openChat(newUser.name); }); await test.step('Expect to be able to close an omnichannel to conversation', async () => { - await agent.poHomeChannel.content.btnCloseChat.click(); - await agent.poHomeChannel.content.inputModalClosingComment.type('any_comment'); - await agent.poHomeChannel.content.btnModalConfirm.click(); - await expect(agent.poHomeChannel.toastSuccess).toBeVisible(); + await agent.poHomeOmnichannel.content.btnCloseChat.click(); + await agent.poHomeOmnichannel.content.inputModalClosingComment.type('any_comment'); + await agent.poHomeOmnichannel.content.btnModalConfirm.click(); + await expect(agent.poHomeOmnichannel.toastSuccess).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts index cf1fd3fe2d5..1f3b2955ae3 100644 --- a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts @@ -2,13 +2,13 @@ import { faker } from '@faker-js/faker'; import type { Browser, Page } from '@playwright/test'; import { test, expect } from './utils/test'; -import { HomeChannel, OmnichannelLiveChat } from './page-objects'; +import { HomeOmnichannel, OmnichannelLiveChat } from './page-objects'; -const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeOmnichannel: HomeOmnichannel }> => { const page = await browser.newPage({ storageState }); - const poHomeChannel = new HomeChannel(page); + const poHomeOmnichannel = new HomeOmnichannel(page); await page.goto('/'); - return { page, poHomeChannel }; + return { page, poHomeOmnichannel }; }; const newUser = { @@ -16,33 +16,28 @@ const newUser = { email: faker.internet.email(), }; test.describe('Livechat', () => { - test.describe('Send message from user', () => { + test.describe('Send message', () => { + let poAuxContext: { page: Page; poHomeOmnichannel: HomeOmnichannel }; let poLiveChat: OmnichannelLiveChat; + let page: Page; - test.beforeEach(async ({ page }) => { - await page.goto('/livechat'); + test.beforeAll(async ({ browser, api }) => { + await api.post('/livechat/users/agent', { username: 'user1' }); + page = await browser.newPage(); poLiveChat = new OmnichannelLiveChat(page); + poAuxContext = await createAuxContext(browser, 'user1-session.json'); + + await page.goto('/livechat'); }); - test('expect send message to live chat', async () => { - await poLiveChat.btnOpenLiveChat('L').click(); - await poLiveChat.sendMessage(newUser); + test.afterAll(async ({ api }) => { + await api.delete('/livechat/users/agent/user1'); + await poAuxContext.page.close(); }); test.describe('Send message to online agent', () => { - let poAuxContext: { page: Page; poHomeChannel: HomeChannel }; - test.beforeAll(async ({ browser, api }) => { - await api.post('/livechat/users/agent', { username: 'user1' }); - - poAuxContext = await createAuxContext(browser, 'user1-session.json'); - }); - - test.afterAll(async () => { - await poAuxContext.page.close(); - }); - - test('expect message is received from agent and user ', async ({ page }) => { + test('Expect message to be sent by livechat', async () => { await poLiveChat.btnOpenLiveChat('R').click(); await poLiveChat.sendMessage(newUser, false); @@ -52,20 +47,42 @@ test.describe('Livechat', () => { await expect(page.locator('div >>text="this_a_test_message_from_user"')).toBeVisible(); }); - test('expect after user close live chat screen dont show messages', async ({ page }) => { + test('expect message to be received by agent', async () => { + await poAuxContext.poHomeOmnichannel.sidenav.openChat(newUser.name); + await expect(poAuxContext.poHomeOmnichannel.content.lastUserMessage).toBeVisible(); + await expect(poAuxContext.poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); + }); + }); + + test.describe('Send message to livechat costumer', () => { + test('Expect message to be sent by agent', async () => { + await poAuxContext.poHomeOmnichannel.content.sendMessage('this_a_test_message_from_agent'); + await expect(page.locator('div >>text="this_a_test_message_from_agent"')).toBeVisible(); + }); + + test('Expect when user minimizes the livechat screen, the composer should be hidden', async () => { await poLiveChat.btnOpenLiveChat('R').click(); await expect(page.locator('[contenteditable="true"]')).not.toBeVisible(); }); + + test('expect message to be received by minimized livechat', async () => { + await poAuxContext.poHomeOmnichannel.content.sendMessage('this_a_test_message_again_from_agent'); + await expect(poLiveChat.unreadMessagesBadge(1)).toBeVisible(); + }); + + test('expect unread messages to be visible after a reload', async () => { + await page.reload(); + await expect(poLiveChat.unreadMessagesBadge(1)).toBeVisible(); + }); }); - }); - test.describe('Verify message is received', () => { - test.use({ storageState: 'user1-session.json' }); - test('expect message is received from user', async ({ page }) => { - await page.goto('/'); - const poHomeChannel = new HomeChannel(page); - await poHomeChannel.sidenav.openChat(newUser.name); - await expect(poHomeChannel.content.lastUserMessage).toBeVisible(); - await expect(poHomeChannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); + + test.describe('close livechat conversation', () => { + test('expect livechat conversation to be closed by agent', async () => { + await poAuxContext.poHomeOmnichannel.content.btnCloseChat.click(); + await poAuxContext.poHomeOmnichannel.content.omnichannelCloseChatModal.inputComment.fill('this_is_a_test_comment'); + await poAuxContext.poHomeOmnichannel.content.omnichannelCloseChatModal.btnConfirm.click(); + await expect(poAuxContext.poHomeOmnichannel.toastSuccess).toBeVisible(); + }); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel-transfer-to-another-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel-transfer-to-another-agents.spec.ts index 3373cd9c241..17ab3569c65 100644 --- a/apps/meteor/tests/e2e/omnichannel-transfer-to-another-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-transfer-to-another-agents.spec.ts @@ -2,23 +2,23 @@ import { faker } from '@faker-js/faker'; import type { Browser, Page } from '@playwright/test'; import { test, expect } from './utils/test'; -import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { OmnichannelLiveChat, HomeOmnichannel } from './page-objects'; -const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeOmnichannel: HomeOmnichannel }> => { const page = await browser.newPage({ storageState }); - const poHomeChannel = new HomeChannel(page); + const poHomeOmnichannel = new HomeOmnichannel(page); await page.goto('/'); await page.locator('.main-content').waitFor(); - return { page, poHomeChannel }; + return { page, poHomeOmnichannel }; }; test.describe('omnichannel-transfer-to-another-agent', () => { let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent1: { page: Page; poHomeChannel: HomeChannel }; - let agent2: { page: Page; poHomeChannel: HomeChannel }; + let agent1: { page: Page; poHomeOmnichannel: HomeOmnichannel }; + let agent2: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { // Set user user 1 as manager and agent let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); @@ -37,8 +37,8 @@ test.describe('omnichannel-transfer-to-another-agent', () => { }); test.beforeEach(async ({ page }) => { // make "user-1" online & "user-2" offline so that chat can be automatically routed to "user-1" - await agent1.poHomeChannel.sidenav.switchStatus('online'); - await agent2.poHomeChannel.sidenav.switchStatus('offline'); + await agent1.poHomeOmnichannel.sidenav.switchStatus('online'); + await agent2.poHomeOmnichannel.sidenav.switchStatus('offline'); // start a new chat for each test newVisitor = { @@ -69,33 +69,33 @@ test.describe('omnichannel-transfer-to-another-agent', () => { test('transfer omnichannel chat to another agent', async () => { await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent1.poHomeChannel.sidenav.openChat(newVisitor.name); + await agent1.poHomeOmnichannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to not be able to transfer chat to "user-2" when that user is offline', async () => { - await agent2.poHomeChannel.sidenav.switchStatus('offline'); + await agent2.poHomeOmnichannel.sidenav.switchStatus('offline'); - await agent1.poHomeChannel.content.btnForwardChat.click(); - await agent1.poHomeChannel.content.inputModalAgentUserName.type('user2'); + await agent1.poHomeOmnichannel.content.btnForwardChat.click(); + await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2'); await expect(agent1.page.locator('text=Empty')).toBeVisible(); await agent1.page.goto('/'); }); await test.step('Expect to be able to transfer an omnichannel to conversation to agent 2 as agent 1 when agent 2 is online', async () => { - await agent2.poHomeChannel.sidenav.switchStatus('online'); + await agent2.poHomeOmnichannel.sidenav.switchStatus('online'); - await agent1.poHomeChannel.sidenav.openChat(newVisitor.name); - await agent1.poHomeChannel.content.btnForwardChat.click(); - await agent1.poHomeChannel.content.inputModalAgentUserName.type('user2'); + await agent1.poHomeOmnichannel.sidenav.openChat(newVisitor.name); + await agent1.poHomeOmnichannel.content.btnForwardChat.click(); + await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2'); await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2"').click(); - await agent1.poHomeChannel.content.inputModalAgentForwardComment.type('any_comment'); - await agent1.poHomeChannel.content.btnModalConfirm.click(); - await expect(agent1.poHomeChannel.toastSuccess).toBeVisible(); + await agent1.poHomeOmnichannel.content.inputModalAgentForwardComment.type('any_comment'); + await agent1.poHomeOmnichannel.content.btnModalConfirm.click(); + await expect(agent1.poHomeOmnichannel.toastSuccess).toBeVisible(); }); await test.step('Expect to have 1 omnichannel assigned to agent 2', async () => { - await agent2.poHomeChannel.sidenav.openChat(newVisitor.name); + await agent2.poHomeOmnichannel.sidenav.openChat(newVisitor.name); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 23d05ba374b..5970cd4a7d4 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -3,7 +3,7 @@ import fs from 'fs/promises'; import type { Locator, Page } from '@playwright/test'; export class HomeContent { - private readonly page: Page; + protected readonly page: Page; constructor(page: Page) { this.page = page; @@ -106,14 +106,6 @@ export class HomeContent { return this.page.locator('[data-qa="UserCard"] a'); } - get btnForwardChat(): Locator { - return this.page.locator('[data-qa-id="ToolBoxAction-balloon-arrow-top-right"]'); - } - - get btnCloseChat(): Locator { - return this.page.locator('[data-qa-id="ToolBoxAction-balloon-close-top-right"]'); - } - get btnContactInformation(): Locator { return this.page.locator('[data-qa-id="ToolBoxAction-user"]'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts new file mode 100644 index 00000000000..307f94273d3 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -0,0 +1,25 @@ +import type { Locator, Page } from '@playwright/test'; + +import { HomeContent } from './home-content'; +import { OmnichannelCloseChatModal } from './omnichannel-close-chat-modal'; + +export class HomeOmnichannelContent extends HomeContent { + readonly omnichannelCloseChatModal: OmnichannelCloseChatModal; + + constructor(page: Page) { + super(page); + this.omnichannelCloseChatModal = new OmnichannelCloseChatModal(page); + } + + get inputMessage(): Locator { + return this.page.locator('[name="msg"]'); + } + + get btnForwardChat(): Locator { + return this.page.locator('[data-qa-id="ToolBoxAction-balloon-arrow-top-right"]'); + } + + get btnCloseChat(): Locator { + return this.page.locator('[data-qa-id="ToolBoxAction-balloon-close-top-right"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index 083829dea7b..e0d7c8e45d9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -1,4 +1,6 @@ export * from './home-content'; +export * from './home-omnichannel-content'; export * from './home-flextab'; export * from './home-sidenav'; export * from './omnichannel-sidenav'; +export * from './omnichannel-close-chat-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts new file mode 100644 index 00000000000..2418c1ebf67 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts @@ -0,0 +1,17 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelCloseChatModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get inputComment(): Locator { + return this.page.locator('input[name="comment"]'); + } + + get btnConfirm(): Locator { + return this.page.locator('//button[contains(text(), "Confirm")]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts new file mode 100644 index 00000000000..b0bec1ea237 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts @@ -0,0 +1,28 @@ +import type { Locator, Page } from '@playwright/test'; + +import { HomeOmnichannelContent, HomeSidenav, HomeFlextab } from './fragments'; + +export class HomeOmnichannel { + private readonly page: Page; + + readonly content: HomeOmnichannelContent; + + readonly sidenav: HomeSidenav; + + readonly tabs: HomeFlextab; + + constructor(page: Page) { + this.page = page; + this.content = new HomeOmnichannelContent(page); + this.sidenav = new HomeSidenav(page); + this.tabs = new HomeFlextab(page); + } + + get toastSuccess(): Locator { + return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); + } + + get btnVerticalBarClose(): Locator { + return this.page.locator('[data-qa="VerticalBarActionClose"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index 0124523a978..043719b93a3 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -10,3 +10,4 @@ export * from './omnichannel-current-chats'; export * from './omnichannel-livechat'; export * from './omnichannel-manager'; export * from './omnichannel-custom-fields'; +export * from './home-omnichannel'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts index 7b19c23ec04..ca5e60c011c 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts @@ -8,7 +8,13 @@ export class OmnichannelLiveChat { } btnOpenLiveChat(label: string): Locator { - return this.page.locator(`[aria-label="${label}"]`); + return this.page.locator(`role=button[name="${label}"]`); + } + + unreadMessagesBadge(count: number): Locator { + const name = count === 1 ? `${count} unread message` : `${count} unread messages`; + + return this.page.locator(`role=status[name="${name}"]`); } get inputName(): Locator { diff --git a/packages/livechat/src/components/Button/index.js b/packages/livechat/src/components/Button/index.js index f1c9f918f67..47205807969 100644 --- a/packages/livechat/src/components/Button/index.js +++ b/packages/livechat/src/components/Button/index.js @@ -1,60 +1,73 @@ +import { withTranslation } from 'react-i18next'; + import { createClassName, memo } from '../helpers'; import styles from './styles.scss'; const handleMouseUp = ({ target }) => target.blur(); -export const Button = memo( - ({ - submit, - disabled, - outline, - nude, - danger, - secondary, - stack, - small, - loading, - badge, - icon, - onClick, - className, - style = {}, - children, - img, - }) => ( - <button - type={submit ? 'submit' : 'button'} - disabled={disabled} - onClick={onClick} - onMouseUp={handleMouseUp} - aria-label={icon ? children[0] : null} - className={createClassName( - styles, - 'button', - { - disabled, - outline, - nude, - danger, - secondary, - stack, - small, - loading, - icon: !!icon, - img, - }, - [className], - )} - style={Object.assign( - {}, - style, - img && { - backgroundImage: `url(${img})`, - }, - )} - > - {badge ? <span className={createClassName(styles, 'button__badge')}>{badge}</span> : null} - {!img && (icon || children)} - </button> +export const Button = withTranslation()( + memo( + ({ + submit, + disabled, + outline, + nude, + danger, + secondary, + stack, + small, + loading, + badge, + icon, + onClick, + className, + style = {}, + children, + img, + t, + }) => ( + <button + type={submit ? 'submit' : 'button'} + disabled={disabled} + onClick={onClick} + onMouseUp={handleMouseUp} + aria-label={icon ? children[0] : null} + className={createClassName( + styles, + 'button', + { + disabled, + outline, + nude, + danger, + secondary, + stack, + small, + loading, + icon: !!icon, + img, + }, + [className], + )} + style={Object.assign( + {}, + style, + img && { + backgroundImage: `url(${img})`, + }, + )} + > + {badge ? ( + <span + role='status' + aria-label={t('unread_messages_count', { count: badge })} + className={createClassName(styles, 'button__badge')} + > + {badge} + </span> + ) : null} + {!img && (icon || children)} + </button> + ), ), ); diff --git a/packages/livechat/src/i18n/en.json b/packages/livechat/src/i18n/en.json index 29451873f0d..9155dd70dea 100644 --- a/packages/livechat/src/i18n/en.json +++ b/packages/livechat/src/i18n/en.json @@ -82,6 +82,8 @@ "the_controller_of_your_personal_data_is_company_na": "The controller of your personal data is [Company Name], with registered office at [Company Address]. To start the chat you agree that your personal data shall be processed and trasmitted in accordance with the General Data Protection Regulation (GDPR).", "transcript_success": "Transcript sent", "type_your_message_here": "Type your message here", + "unread_messages_count_one": "{{count}} unread message", + "unread_messages_count_other": "{{count}} unread messages", "unread_messages": "unread messages", "user_added_by": "User added by", "user_joined": "User joined", diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index 1c7afea23d7..d835b458cb7 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -253,11 +253,6 @@ export const loadMessages = async () => { await initRoom(); await store.setState({ messages: (messages || []).reverse(), noMoreMessages: false, loading: false }); - if (messages && messages.length) { - const lastMessage = messages[messages.length - 1]; - await store.setState({ lastReadMessageId: lastMessage && lastMessage._id }); - } - if (ongoingCall && isCallOngoing(ongoingCall.callStatus)) { return; } diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js index df5eb70d319..112011e0f17 100644 --- a/packages/livechat/src/routes/Chat/container.js +++ b/packages/livechat/src/routes/Chat/container.js @@ -7,7 +7,7 @@ import { ModalManager } from '../../components/Modal'; import { debounce, getAvatarUrl, canRenderMessage, throttle, upsert } from '../../components/helpers'; import { normalizeQueueAlert } from '../../lib/api'; import constants from '../../lib/constants'; -import { loadConfig } from '../../lib/main'; +import { loadConfig, processUnread } from '../../lib/main'; import { parentCall, runCallbackEventEmitter } from '../../lib/parentCall'; import { createToken } from '../../lib/random'; import { initRoom, closeChat, loadMessages, loadMoreMessages, defaultRoomParams, getGreetingMessages } from '../../lib/room'; @@ -315,7 +315,8 @@ class ChatContainer extends Component { async componentDidMount() { await this.checkConnectingAgent(); - loadMessages(); + await loadMessages(); + processUnread(); } async componentDidUpdate(prevProps) { -- GitLab From 21405eb6b414b4ffaaef0ce5ad9f7e0fdca588e9 Mon Sep 17 00:00:00 2001 From: Hugo Costa <hugocarreiracosta@gmail.com> Date: Thu, 22 Sep 2022 11:47:00 -0300 Subject: [PATCH 055/107] [FIX] MIME Type fallback for .mov File Extensions (#26921) --- .../Attachments/Files/VideoAttachment.tsx | 3 ++- .../lib/utils/userAgentMIMETypeFallback.ts | 16 ++++++++++++++++ .../room/modals/FileUploadModal/MediaPreview.tsx | 3 ++- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 apps/meteor/client/lib/utils/userAgentMIMETypeFallback.ts diff --git a/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx b/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx index d1c849f066d..588894bc0b6 100644 --- a/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx +++ b/apps/meteor/client/components/message/Attachments/Files/VideoAttachment.tsx @@ -5,6 +5,7 @@ import colors from '@rocket.chat/fuselage-tokens/colors'; import { useMediaUrl } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; +import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; import MarkdownText from '../../../MarkdownText'; import Attachment from '../Attachment'; import AttachmentContent from '../Attachment/AttachmentContent'; @@ -46,7 +47,7 @@ export const VideoAttachment: FC<VideoAttachmentProps> = ({ {!collapsed && ( <AttachmentContent width='full' className={videoAttachmentCss}> <Box is='video' width='full' controls preload='metadata'> - <source src={getURL(url)} type={type} /> + <source src={getURL(url)} type={userAgentMIMETypeFallback(type)} /> </Box> {description && ( <AttachmentDetails is='figcaption'> diff --git a/apps/meteor/client/lib/utils/userAgentMIMETypeFallback.ts b/apps/meteor/client/lib/utils/userAgentMIMETypeFallback.ts new file mode 100644 index 00000000000..02d5c2ca7ea --- /dev/null +++ b/apps/meteor/client/lib/utils/userAgentMIMETypeFallback.ts @@ -0,0 +1,16 @@ +/* + * Some browsers don't support the MIME type for quicktime video encoder, so we need to + * fallback to the 'video/mp4'. There are other fallbacks for other browsers, but this is + * the only one we need for now. + * @param type - the MIME type to check + * @returns the MIME type to use + */ +export const userAgentMIMETypeFallback = (type: string): string => { + const userAgent = navigator.userAgent.toLocaleLowerCase(); + + if (type === 'video/quicktime' && userAgent.indexOf('safari') !== -1) { + return 'video/mp4'; + } + + return type; +}; diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx index 0a078e5cee4..fd2d05c6e9f 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/MediaPreview.tsx @@ -2,6 +2,7 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useEffect, useState, memo } from 'react'; +import { userAgentMIMETypeFallback } from '../../../../lib/utils/userAgentMIMETypeFallback'; import { FilePreviewType } from './FilePreview'; import ImagePreview from './ImagePreview'; import PreviewSkeleton from './PreviewSkeleton'; @@ -58,7 +59,7 @@ const MediaPreview = ({ file, fileType }: MediaPreviewProps): ReactElement => { if (fileType === FilePreviewType.VIDEO) { return ( <Box is='video' w='full' controls> - <source src={url} type={file.type} /> + <source src={url} type={userAgentMIMETypeFallback(file.type)} /> {t('Browser_does_not_support_video_element')} </Box> ); -- GitLab From 378ab34eddea0cf208ee4bd1deaad1b7d44c133a Mon Sep 17 00:00:00 2001 From: Hugo Costa <hugocarreiracosta@gmail.com> Date: Thu, 22 Sep 2022 13:37:55 -0300 Subject: [PATCH 056/107] [NEW] Add Markup to QuoteAttachment (#26751) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Filipe Marins <9275105+filipemarins@users.noreply.github.com> Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .../client/imports/components/messages.css | 7 + .../Attachment/AttachmentDetails.tsx | 2 +- .../Attachments/Attachments.stories.tsx | 7 + .../message/Attachments/Attachments.tsx | 9 +- .../components/message/Attachments/Item.tsx | 11 +- .../message/Attachments/QuoteAttachment.tsx | 53 ++++--- .../client/lib/utils/mapMessageFromApi.ts | 2 +- .../room/MessageList/components/Message.tsx | 3 +- .../MessageList/components/MessageContent.tsx | 15 +- .../components/MessageContentBody.tsx | 128 ++++++++-------- .../components/ThreadMessagePreviewBody.tsx | 6 +- .../room/MessageList/hooks/useMessages.ts | 69 ++++----- .../hooks/useParsedMessage.spec.tsx | 86 ----------- .../MessageList/hooks/useParsedMessage.ts | 38 ----- .../room/MessageList/lib/isParsedMessage.ts | 3 + .../lib/parseMessageTextToAstMarkdown.ts | 110 ++++++++++++++ .../page-objects/fragments/home-content.ts | 2 +- .../room/MessageList/MessageContent.spec.tsx | 12 ++ .../lib/isMessageFirstUnread.spec.ts | 4 +- .../MessageList/lib/isMessageNewDay.spec.ts | 5 +- .../lib/isMessageSequential.spec.ts | 6 +- .../MessageList/lib/isOwnUserMessage.spec.ts | 6 +- .../MessageList/lib/isParsedMessage.spec.ts | 51 +++++++ .../room/MessageList/lib/parseMessage.spec.ts | 137 ++++++++++++++++++ .../core-typings/src/IMessage/IMessage.ts | 4 +- .../MessageAttachmentAction.ts | 3 + .../MessageAttachmentBase.ts | 3 + .../MessageAttachmentDefault.ts | 3 + .../MessageQuoteAttachment.ts | 3 + .../src/elements/PreviewInlineElements.tsx | 11 +- 30 files changed, 512 insertions(+), 287 deletions(-) delete mode 100644 apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.spec.tsx delete mode 100644 apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts create mode 100644 apps/meteor/client/views/room/MessageList/lib/isParsedMessage.ts create mode 100644 apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts rename apps/meteor/{ => tests/unit}/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts (88%) rename apps/meteor/{ => tests/unit}/client/views/room/MessageList/lib/isMessageNewDay.spec.ts (83%) rename apps/meteor/{ => tests/unit}/client/views/room/MessageList/lib/isMessageSequential.spec.ts (94%) rename apps/meteor/{ => tests/unit}/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts (84%) create mode 100644 apps/meteor/tests/unit/client/views/room/MessageList/lib/isParsedMessage.spec.ts create mode 100644 apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts diff --git a/apps/meteor/app/theme/client/imports/components/messages.css b/apps/meteor/app/theme/client/imports/components/messages.css index d1b8bb5ddaa..54218254199 100644 --- a/apps/meteor/app/theme/client/imports/components/messages.css +++ b/apps/meteor/app/theme/client/imports/components/messages.css @@ -310,3 +310,10 @@ } } } + +.message-popup .rcx-message-attachment { + overflow-y: auto !important; + + max-width: 100% !important; + max-height: 200px !important; +} diff --git a/apps/meteor/client/components/message/Attachments/Attachment/AttachmentDetails.tsx b/apps/meteor/client/components/message/Attachments/Attachment/AttachmentDetails.tsx index 8f15f3b0831..007829a9697 100644 --- a/apps/meteor/client/components/message/Attachments/Attachment/AttachmentDetails.tsx +++ b/apps/meteor/client/components/message/Attachments/Attachment/AttachmentDetails.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import React, { FC, ComponentProps } from 'react'; const AttachmentDetails: FC<ComponentProps<typeof Box>> = ({ ...props }) => ( - <Box rcx-attachment__details fontScale='p2' color='info' bg='neutral-100' pi='x16' pb='x16' {...props} /> + <Box rcx-attachment__details fontScale='p2' color='info' bg='neutral-200' pi='x16' pb='x16' {...props} /> ); export default AttachmentDetails; diff --git a/apps/meteor/client/components/message/Attachments/Attachments.stories.tsx b/apps/meteor/client/components/message/Attachments/Attachments.stories.tsx index 16d60bb1093..73e29b0559c 100644 --- a/apps/meteor/client/components/message/Attachments/Attachments.stories.tsx +++ b/apps/meteor/client/components/message/Attachments/Attachments.stories.tsx @@ -83,6 +83,13 @@ const audio: FileAttachmentProps = { const message = { _id: '12312321', rid: 'GENERAL', + ts: new Date('2016-12-09T16:53:06.761Z'), + u: { + _id: 'rocket.cat', + name: 'rocket.cat', + username: 'rocket.cat', + }, + _updatedAt: new Date(), msg: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', diff --git a/apps/meteor/client/components/message/Attachments/Attachments.tsx b/apps/meteor/client/components/message/Attachments/Attachments.tsx index 5e158586940..dee083f9410 100644 --- a/apps/meteor/client/components/message/Attachments/Attachments.tsx +++ b/apps/meteor/client/components/message/Attachments/Attachments.tsx @@ -1,10 +1,15 @@ import { FileProp, MessageAttachmentBase } from '@rocket.chat/core-typings'; -import React, { FC } from 'react'; +import React, { ReactElement } from 'react'; import { useBlockRendered } from '../hooks/useBlockRendered'; import Item from './Item'; -const Attachments: FC<{ attachments: Array<MessageAttachmentBase>; file?: FileProp }> = ({ attachments = null, file }): any => { +type AttachmentsProps = { + file?: FileProp; + attachments: MessageAttachmentBase[]; +}; + +const Attachments = ({ attachments, file }: AttachmentsProps): ReactElement => { const { className, ref } = useBlockRendered<HTMLDivElement>(); return ( <> diff --git a/apps/meteor/client/components/message/Attachments/Item.tsx b/apps/meteor/client/components/message/Attachments/Item.tsx index 85dfd59e8f5..8e144cf82e3 100644 --- a/apps/meteor/client/components/message/Attachments/Item.tsx +++ b/apps/meteor/client/components/message/Attachments/Item.tsx @@ -1,17 +1,22 @@ import { isFileAttachment, FileProp, MessageAttachmentBase, isQuoteAttachment } from '@rocket.chat/core-typings'; -import React, { FC, memo } from 'react'; +import React, { memo, ReactElement } from 'react'; import DefaultAttachment from './DefaultAttachment'; import { FileAttachment } from './Files'; import { QuoteAttachment } from './QuoteAttachment'; -const Item: FC<{ attachment: MessageAttachmentBase; file?: FileProp | undefined }> = ({ attachment, file }) => { +type AttachmentsItemProps = { + file?: FileProp; + attachment: MessageAttachmentBase; +}; + +const Item = ({ attachment, file }: AttachmentsItemProps): ReactElement => { if (isFileAttachment(attachment) && file) { return <FileAttachment {...attachment} file={file} />; } if (isQuoteAttachment(attachment)) { - return <QuoteAttachment {...attachment} />; + return <QuoteAttachment attachment={attachment} />; } return <DefaultAttachment {...(attachment as any)} />; diff --git a/apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx index 2ef7f40ca83..916c5d0d5db 100644 --- a/apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/Attachments/QuoteAttachment.tsx @@ -2,11 +2,11 @@ import { MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import React, { FC } from 'react'; +import React, { ReactElement } from 'react'; import Attachments from '.'; import { useTimeAgo } from '../../../hooks/useTimeAgo'; -import MarkdownText from '../../MarkdownText'; +import MessageContentBody from '../../../views/room/MessageList/components/MessageContentBody'; import AttachmentAuthor from './Attachment/AttachmentAuthor'; import AttachmentAuthorAvatar from './Attachment/AttachmentAuthorAvatar'; import AttachmentAuthorName from './Attachment/AttachmentAuthorName'; @@ -14,7 +14,12 @@ import AttachmentContent from './Attachment/AttachmentContent'; import AttachmentDetails from './Attachment/AttachmentDetails'; import AttachmentInner from './Attachment/AttachmentInner'; -const hover = css` +const quoteStyles = css` + .rcx-attachment__details { + .rcx-message-body { + color: ${colors.n700} !important; + } + } &:hover, &:focus { .rcx-attachment__details { @@ -25,42 +30,44 @@ const hover = css` } `; -export const QuoteAttachment: FC<MessageQuoteAttachment> = ({ - author_icon: url, - author_name: name, - author_link: authorLink, - message_link: messageLink, - ts, - text, - attachments, -}) => { +type QuoteAttachmentProps = { + attachment: MessageQuoteAttachment; +}; + +export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElement => { const format = useTimeAgo(); + return ( <> - <AttachmentContent className={hover} width='full'> + <AttachmentContent className={quoteStyles} width='full'> <AttachmentDetails is='blockquote' borderRadius='x2' borderWidth='x2' borderStyle='solid' borderColor='neutral-200' - borderInlineStartColor='neutral-600' + borderInlineStartColor='neutral-500' > <AttachmentAuthor> - <AttachmentAuthorAvatar url={url} /> - <AttachmentAuthorName {...(authorLink && { is: 'a', href: authorLink, target: '_blank', color: undefined })}> - {name} + <AttachmentAuthorAvatar url={attachment.author_icon} /> + <AttachmentAuthorName + {...(attachment.author_name && { is: 'a', href: attachment.author_link, target: '_blank', color: 'info' })} + > + {attachment.author_name} </AttachmentAuthorName> - {ts && ( - <Box fontScale='c1' {...(messageLink ? { is: 'a', href: messageLink } : { color: 'hint' })}> - {format(ts)} + {attachment.ts && ( + <Box + fontScale='c1' + {...(attachment.message_link ? { is: 'a', href: attachment.message_link, color: 'hint' } : { color: 'hint' })} + > + {format(attachment.ts)} </Box> )} </AttachmentAuthor> - <MarkdownText parseEmoji variant='document' content={text} /> - {attachments && ( + {attachment.md ? <MessageContentBody md={attachment.md} /> : attachment.text} + {attachment.attachments && ( <AttachmentInner> - <Attachments attachments={attachments} /> + <Attachments attachments={attachment.attachments} /> </AttachmentInner> )} </AttachmentDetails> diff --git a/apps/meteor/client/lib/utils/mapMessageFromApi.ts b/apps/meteor/client/lib/utils/mapMessageFromApi.ts index 69e80339b62..c79d161eac1 100644 --- a/apps/meteor/client/lib/utils/mapMessageFromApi.ts +++ b/apps/meteor/client/lib/utils/mapMessageFromApi.ts @@ -10,7 +10,7 @@ export const mapMessageFromApi = ({ attachments, tlm, ts, _updatedAt, webRtcCall ...(attachments && { attachments: attachments.map(({ ts, ...attachment }) => ({ ...(ts && { ts: new Date(ts) }), - ...attachment, + ...(attachment as any), })), }), }); diff --git a/apps/meteor/client/views/room/MessageList/components/Message.tsx b/apps/meteor/client/views/room/MessageList/components/Message.tsx index c3e76d0363f..5bb3003e270 100644 --- a/apps/meteor/client/views/room/MessageList/components/Message.tsx +++ b/apps/meteor/client/views/room/MessageList/components/Message.tsx @@ -8,6 +8,7 @@ import UserAvatar from '../../../../components/avatar/UserAvatar'; import { useMessageActions } from '../../contexts/MessageContext'; import { useIsMessageHighlight } from '../contexts/MessageHighlightContext'; import { useIsSelecting, useToggleSelect, useIsSelectedMessage, useCountSelected } from '../contexts/SelectedMessagesContext'; +import { MessageWithMdEnforced } from '../lib/parseMessageTextToAstMarkdown'; import MessageContent from './MessageContent'; import MessageContentIgnored from './MessageContentIgnored'; import MessageHeader from './MessageHeader'; @@ -15,7 +16,7 @@ import { MessageIndicators } from './MessageIndicators'; import Toolbox from './Toolbox'; const Message: FC<{ - message: IMessage; + message: MessageWithMdEnforced; sequential: boolean; id: IMessage['_id']; unread: boolean; diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx index 94dbe18aa41..7d298e505fb 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContent.tsx @@ -16,13 +16,14 @@ import MessageLocation from '../../../location/MessageLocation'; import { useMessageActions, useMessageOembedIsEnabled, useMessageRunActionLink } from '../../contexts/MessageContext'; import { useTranslateAttachments, useMessageListShowReadReceipt } from '../contexts/MessageListContext'; import { isOwnUserMessage } from '../lib/isOwnUserMessage'; +import { MessageWithMdEnforced } from '../lib/parseMessageTextToAstMarkdown'; import MessageContentBody from './MessageContentBody'; import ReactionsList from './MessageReactionsList'; import ReadReceipt from './MessageReadReceipt'; import PreviewList from './UrlPreview'; const MessageContent: FC<{ - message: IMessage; + message: MessageWithMdEnforced; sequential: boolean; subscription?: ISubscription; id: IMessage['_id']; @@ -53,19 +54,23 @@ const MessageContent: FC<{ return ( <> - {!message.blocks && (message.md || message.msg) && ( + {!message.blocks && message.md && ( <MessageBody data-qa-type='message-body'> - {!isEncryptedMessage && <MessageContentBody message={message} />} - {isEncryptedMessage && message.e2e === 'done' && <MessageContentBody message={message} />} + {!isEncryptedMessage && <MessageContentBody md={message.md} mentions={message.mentions} channels={message.channels} />} + {isEncryptedMessage && message.e2e === 'done' && ( + <MessageContentBody md={message.md} mentions={message.mentions} channels={message.channels} /> + )} {isEncryptedMessage && message.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} </MessageBody> )} + {message.blocks && ( <MessageBlock fixedWidth> <MessageBlockUiKit mid={message._id} blocks={message.blocks} appId rid={message.rid} /> </MessageBlock> )} - {messageAttachments && <Attachments attachments={messageAttachments} file={message.file} />} + + {!!messageAttachments.length && <Attachments attachments={messageAttachments} file={message.file} />} {oembedIsEnabled && !!message.urls?.length && <PreviewList urls={message.urls} />} diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx index 0bf591ac685..6a30b8f8fa3 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx @@ -1,6 +1,5 @@ -import { IMessage } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, MessageBody } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { MarkupInteractionContext, Markup, UserMention, ChannelMention } from '@rocket.chat/gazzodown'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -9,7 +8,9 @@ import React, { ReactElement, useCallback, useMemo } from 'react'; import { emoji } from '../../../../../app/emoji/client'; import { useMessageActions } from '../../contexts/MessageContext'; import { useMessageListHighlights } from '../contexts/MessageListContext'; -import { useParsedMessage } from '../hooks/useParsedMessage'; +import { MessageWithMdEnforced } from '../lib/parseMessageTextToAstMarkdown'; + +type MessageContentBodyProps = Pick<MessageWithMdEnforced, 'mentions' | 'channels' | 'md'>; const detectEmoji = (text: string): { name: string; className: string; image?: string; content: string }[] => { const html = Object.values(emoji.packages) @@ -26,13 +27,7 @@ const detectEmoji = (text: string): { name: string; className: string; image?: s })); }; -type MessageContentBodyProps = { - message: IMessage; -}; - -const MessageContentBody = ({ message }: MessageContentBodyProps): ReactElement => { - const tokens = useParsedMessage(message); - +const MessageContentBody = ({ mentions, channels, md }: MessageContentBodyProps): ReactElement => { const highlights = useMessageListHighlights(); const highlightRegex = useMemo(() => { if (!highlights || !highlights.length) { @@ -45,21 +40,21 @@ const MessageContentBody = ({ message }: MessageContentBodyProps): ReactElement return (): RegExp => new RegExp(expression, 'gmi'); }, [highlights]); - const { - actions: { openRoom, openUserCard }, - } = useMessageActions(); - const resolveUserMention = useCallback( (mention: string) => { if (mention === 'all' || mention === 'here') { return undefined; } - return message.mentions?.find(({ username }) => username === mention); + return mentions?.find(({ username }) => username === mention); }, - [message.mentions], + [mentions], ); + const { + actions: { openRoom, openUserCard }, + } = useMessageActions(); + const onUserMentionClick = useCallback( ({ username }: UserMention) => { if (!username) { @@ -71,61 +66,60 @@ const MessageContentBody = ({ message }: MessageContentBodyProps): ReactElement [openUserCard], ); - const resolveChannelMention = useCallback( - (mention: string) => message.channels?.find(({ name }) => name === mention), - [message.channels], - ); + const resolveChannelMention = useCallback((mention: string) => channels?.find(({ name }) => name === mention), [channels]); const onChannelMentionClick = useCallback(({ _id: rid }: ChannelMention) => openRoom(rid), [openRoom]); + // TODO: this style should go to Fuselage <MessageBody> repository + const messageBodyAdditionalStyles = css` + > blockquote { + padding-inline: 8px; + border-radius: 2px; + border-width: 2px; + border-style: solid; + background-color: var(--rcx-color-neutral-100, ${colors.n100}); + border-color: var(--rcx-color-neutral-200, ${colors.n200}); + border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); + + &:hover, + &:focus { + background-color: var(--rcx-color-neutral-200, ${colors.n200}); + border-color: var(--rcx-color-neutral-300, ${colors.n300}); + border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); + } + } + > ul.task-list { + > li::before { + display: none; + } + + > li > .rcx-check-box > .rcx-check-box__input:focus + .rcx-check-box__fake { + z-index: 1; + } + + list-style: none; + margin-inline-start: 0; + padding-inline-start: 0; + } + `; + return ( - <Box - className={css` - > blockquote { - padding-inline: 8px; - border-radius: 2px; - border-width: 2px; - border-style: solid; - background-color: var(--rcx-color-neutral-100, ${colors.n100}); - border-color: var(--rcx-color-neutral-200, ${colors.n200}); - border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); - - &:hover, - &:focus { - background-color: var(--rcx-color-neutral-200, ${colors.n200}); - border-color: var(--rcx-color-neutral-300, ${colors.n300}); - border-inline-start-color: var(--rcx-color-neutral-600, ${colors.n600}); - } - } - - > ul.task-list { - > li::before { - display: none; - } - - > li > .rcx-check-box > .rcx-check-box__input:focus + .rcx-check-box__fake { - z-index: 1; - } - - list-style: none; - margin-inline-start: 0; - padding-inline-start: 0; - } - `} - > - <MarkupInteractionContext.Provider - value={{ - detectEmoji, - highlightRegex, - resolveUserMention, - onUserMentionClick, - resolveChannelMention, - onChannelMentionClick, - }} - > - <Markup tokens={tokens} /> - </MarkupInteractionContext.Provider> - </Box> + <MessageBody> + <Box className={messageBodyAdditionalStyles}> + <MarkupInteractionContext.Provider + value={{ + detectEmoji, + highlightRegex, + resolveUserMention, + onUserMentionClick, + resolveChannelMention, + onChannelMentionClick, + }} + > + <Markup tokens={md} /> + </MarkupInteractionContext.Provider> + </Box> + </MessageBody> ); }; diff --git a/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx index 77424e61118..80cbe550b07 100644 --- a/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx +++ b/apps/meteor/client/views/room/MessageList/components/ThreadMessagePreviewBody.tsx @@ -2,16 +2,16 @@ import { IMessage } from '@rocket.chat/core-typings'; import { PreviewMarkup } from '@rocket.chat/gazzodown'; import React, { ReactElement } from 'react'; -import { useParsedMessage } from '../hooks/useParsedMessage'; +import { parseMessageTextToAstMarkdown } from '../lib/parseMessageTextToAstMarkdown'; type ThreadMessagePreviewBodyProps = { message: IMessage; }; const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps): ReactElement => { - const tokens = useParsedMessage(message); + const parsedMessage = parseMessageTextToAstMarkdown(message, { colors: true, emoticons: true }); - return <PreviewMarkup tokens={tokens} />; + return parsedMessage.md ? <PreviewMarkup tokens={parsedMessage.md} /> : <></>; }; export default ThreadMessagePreviewBody; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts index 74e2a256cd9..8320199ed83 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts @@ -1,11 +1,15 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { IMessage } from '@rocket.chat/core-typings'; +import { IRoom, IMessage } from '@rocket.chat/core-typings'; import { Mongo } from 'meteor/mongo'; import { useCallback, useMemo } from 'react'; import { ChatMessage } from '../../../../../app/models/client'; -// import { useSetting } from '@rocket.chat/ui-contexts' import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { useMessageListContext } from '../contexts/MessageListContext'; +import { + MessageWithMdEnforced, + parseMessageTextToAstMarkdown, + removePossibleNullMessageValues, +} from '../lib/parseMessageTextToAstMarkdown'; const options = { sort: { @@ -13,41 +17,24 @@ const options = { }, }; -const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; +export const useMessages = ({ rid }: { rid: IRoom['_id'] }): MessageWithMdEnforced[] => { + const { autoTranslateLanguage, katex, showColors, useShowTranslated } = useMessageListContext(); + + const normalizeMessage = useMemo(() => { + const parseOptions = { + colors: showColors, + emoticons: true, + ...(Boolean(katex) && { + katex: { + dollarSyntax: katex?.dollarSyntaxEnabled, + parenthesisSyntax: katex?.parenthesisSyntaxEnabled, + }, + }), + }; + return (message: IMessage): MessageWithMdEnforced => + parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateLanguage, useShowTranslated); + }, [autoTranslateLanguage, katex, showColors, useShowTranslated]); -// In a previous version of the app, some values were being set to null. -// This is a workaround to remove those null values. -// A migration script should be created to remove this code. -const removePossibleNullValues = ({ - editedBy, - editedAt, - emoji, - avatar, - alias, - customFields, - groupable, - attachments, - reactions, - ...message -}: any): IMessage => ({ - ...message, - ...(isNotNullOrUndefined(editedBy) && { editedBy }), - ...(isNotNullOrUndefined(editedAt) && { editedAt }), - ...(isNotNullOrUndefined(emoji) && { emoji }), - ...(isNotNullOrUndefined(avatar) && { avatar }), - ...(isNotNullOrUndefined(alias) && { alias }), - ...(isNotNullOrUndefined(customFields) && { customFields }), - ...(isNotNullOrUndefined(groupable) && { groupable }), - ...(isNotNullOrUndefined(attachments) && { attachments }), - ...(isNotNullOrUndefined(reactions) && { reactions }), -}); - -export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { - // const hideSettings = !!useSetting('Hide_System_Messages'); - - // const room = Rooms.findOne(rid, { fields: { sysMes: 1 } }); - // const settingValues = Array.isArray(room.sysMes) ? room.sysMes : hideSettings || []; - // const hideMessagesOfType = new Set(settingValues.reduce((array, value) => [...array, ...value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]], [])); const query: Mongo.Query<IMessage> = useMemo( () => ({ rid, @@ -57,9 +44,7 @@ export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { [rid], ); - // if (hideMessagesOfType.size) { - // query.t = { $nin: Array.from(hideMessagesOfType.values()) }; - // } - - return useReactiveValue<IMessage[]>(useCallback(() => ChatMessage.find(query, options).fetch().map(removePossibleNullValues), [query])); + return useReactiveValue<MessageWithMdEnforced[]>( + useCallback(() => ChatMessage.find(query, options).fetch().map(normalizeMessage), [query, normalizeMessage]), + ); }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.spec.tsx b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.spec.tsx deleted file mode 100644 index 75569d5778f..00000000000 --- a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; -import sinon from 'sinon'; - -const date = new Date('2021-10-27T00:00:00.000Z'); -const baseMessage = { - ts: date, - u: { - _id: 'userId', - name: 'userName', - username: 'userName', - }, - msg: 'message', - rid: 'roomId', - _id: 'messageId', - _updatedAt: date, - urls: [], -}; - -const baseStubs = { - './useAutotranslateLanguage': { - useAutotranslateLanguage: (): boolean => false, - }, - '@rocket.chat/ui-contexts': { - useSetting: (): boolean => false, - }, -}; - -describe('useParsedMessage', () => { - it('should call the parse function with message and parse options parameters if all settings is false', () => { - const messageParser = sinon.spy(); - const { useParsedMessage } = proxyquire.noCallThru().load('./useParsedMessage', { - ...baseStubs, - '@rocket.chat/message-parser': { - parse: messageParser, - }, - }); - renderHook(() => useParsedMessage(baseMessage)); - - expect(messageParser.calledOnceWith(baseMessage.msg, { colors: false, emoticons: true })).to.be.true; - }); - - it('should call the parse function with katex in options parameters if Katex_Enabled is true', () => { - const messageParser = sinon.spy(); - const { useParsedMessage } = proxyquire.noCallThru().load('./useParsedMessage', { - ...baseStubs, - '@rocket.chat/ui-contexts': { - useSetting: (setting: string): boolean => setting === 'Katex_Enabled', - }, - '@rocket.chat/message-parser': { - parse: messageParser, - }, - }); - renderHook(() => useParsedMessage(baseMessage)); - - expect( - messageParser.calledOnceWith(baseMessage.msg, { - colors: false, - emoticons: true, - katex: { dollarSyntax: false, parenthesisSyntax: false }, - }), - ).to.be.true; - }); - - it('should call the parse function without katex in options parameters if Katex_Enabled is false', () => { - const messageParser = sinon.spy(); - const { useParsedMessage } = proxyquire.noCallThru().load('./useParsedMessage', { - ...baseStubs, - '@rocket.chat/ui-contexts': { - useSetting: (setting: string): boolean => setting !== 'Katex_Enabled', - }, - '@rocket.chat/message-parser': { - parse: messageParser, - }, - }); - renderHook(() => useParsedMessage(baseMessage)); - - expect( - messageParser.calledOnceWith(baseMessage.msg, { - colors: true, - emoticons: true, - }), - ).to.be.true; - }); -}); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts deleted file mode 100644 index 5bf35a717e3..00000000000 --- a/apps/meteor/client/views/room/MessageList/hooks/useParsedMessage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IMessage, isTranslatedMessage, ITranslatedMessage } from '@rocket.chat/core-typings'; -import { Root, parse } from '@rocket.chat/message-parser'; -import { useMemo } from 'react'; - -import { useMessageListContext, useShowTranslated } from '../contexts/MessageListContext'; - -export function useParsedMessage(message: IMessage & Partial<ITranslatedMessage>): Root { - const { autoTranslateLanguage, katex, showColors } = useMessageListContext(); - const translated = useShowTranslated({ message }); - const translations = isTranslatedMessage(message) && message.translations; - const { md, msg } = message; - - return useMemo(() => { - const parseOptions = { - colors: showColors, - emoticons: true, - ...(katex && { - katex: { - dollarSyntax: katex.dollarSyntaxEnabled, - parenthesisSyntax: katex.parenthesisSyntaxEnabled, - }, - }), - }; - - if (translated && autoTranslateLanguage && translations) { - return parse(translations[autoTranslateLanguage], parseOptions); - } - if (md) { - return md; - } - - if (!msg) { - return []; - } - - return parse(msg, parseOptions); - }, [showColors, katex, autoTranslateLanguage, md, msg, translated, translations]); -} diff --git a/apps/meteor/client/views/room/MessageList/lib/isParsedMessage.ts b/apps/meteor/client/views/room/MessageList/lib/isParsedMessage.ts new file mode 100644 index 00000000000..d45d7765692 --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/lib/isParsedMessage.ts @@ -0,0 +1,3 @@ +import { Root } from '@rocket.chat/message-parser'; + +export const isParsedMessage = (text: string | Root): text is Root => Array.isArray(text) && text.length > 0; diff --git a/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts new file mode 100644 index 00000000000..6fc56660fbc --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/lib/parseMessageTextToAstMarkdown.ts @@ -0,0 +1,110 @@ +import { IMessage, isQuoteAttachment, isTranslatedMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; +import { Options, parse, Root } from '@rocket.chat/message-parser'; + +import { isParsedMessage } from './isParsedMessage'; + +type WithRequiredProperty<Type, Key extends keyof Type> = Type & { + [Property in Key]-?: Type[Property]; +}; + +export type MessageWithMdEnforced = WithRequiredProperty<IMessage, 'md'>; +/* + * Removes null values for known properties values. + * Adds a property `md` to the message with the parsed message if is not provided. + * if has `attachments` property, but attachment is missing `md` property, it will be added. + * if translation is enabled and message contains `translations` property, it will be replaced by the parsed message. + * @param message The message to be parsed. + * @param parseOptions The options to be used in the parser. + * @param autoTranslateLanguage The language to be used in the parser. + * @param showTranslatedMessage function that evaluates if message should be translated. + * @returns message normalized. + */ + +export const parseMessageTextToAstMarkdown = ( + message: IMessage, + parseOptions: Options, + autoTranslateLanguage?: string, + showTranslated?: ({ message }: { message: IMessage }) => boolean, +): MessageWithMdEnforced => { + const msg = removePossibleNullMessageValues(message); + const translations = autoTranslateLanguage && showTranslated && isTranslatedMessage(msg) && msg.translations; + const translated = autoTranslateLanguage && showTranslated?.({ message }); + + const text = (translated && translations && translations[autoTranslateLanguage]) || msg.msg; + + return { + ...msg, + md: msg.md ?? textToMessageToken(text, parseOptions), + ...(msg.attachments && { attachments: parseMessageAttachments(msg.attachments, parseOptions) }), + }; +}; + +const parseMessageQuoteAttachment = <T extends MessageQuoteAttachment>(quote: T, parseOptions: Options): T => { + if (quote.attachments && quote.attachments?.length > 0) { + quote.attachments = quote.attachments.map((attachment) => parseMessageQuoteAttachment(attachment, parseOptions)); + } + + return { ...quote, md: quote.md ?? textToMessageToken(quote.text, parseOptions) }; +}; + +const parseMessageAttachments = <T extends MessageAttachment>(attachments: T[], parseOptions: Options): T[] => { + if (attachments.length === 0) { + return attachments; + } + + return attachments.map((attachment) => { + if (isQuoteAttachment(attachment) && attachment.attachments) { + attachment.attachments = attachment.attachments.map((quoteAttachment) => parseMessageQuoteAttachment(quoteAttachment, parseOptions)); + } + + if (!attachment.text) { + return attachment; + } + + return { + ...attachment, + md: attachment.md ?? textToMessageToken(attachment.text, parseOptions), + }; + }); +}; + +const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; + +// In a previous version of the app, some values were being set to null. +// This is a workaround to remove those null values. +// A migration script should be created to remove this code. +export const removePossibleNullMessageValues = ({ + editedBy, + editedAt, + emoji, + avatar, + alias, + customFields, + groupable, + attachments, + reactions, + ...message +}: any): IMessage => ({ + ...message, + ...(isNotNullOrUndefined(editedBy) && { editedBy }), + ...(isNotNullOrUndefined(editedAt) && { editedAt }), + ...(isNotNullOrUndefined(emoji) && { emoji }), + ...(isNotNullOrUndefined(avatar) && { avatar }), + ...(isNotNullOrUndefined(alias) && { alias }), + ...(isNotNullOrUndefined(customFields) && { customFields }), + ...(isNotNullOrUndefined(groupable) && { groupable }), + ...(isNotNullOrUndefined(attachments) && { attachments }), + ...(isNotNullOrUndefined(reactions) && { reactions }), +}); + +const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): Root => { + if (!textOrRoot) { + return []; + } + + if (isParsedMessage(textOrRoot)) { + return textOrRoot; + } + + return parse(textOrRoot, parseOptions); +}; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 5970cd4a7d4..4072e9a6e6e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -67,7 +67,7 @@ export class HomeContent { } get waitForLastMessageTextAttachmentEqualsText(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-box--with-inline-elements'); + return this.page.locator('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-message-body'); } get btnOptionEditMessage(): Locator { diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx b/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx index 8715f92d16b..06aa0d851b9 100644 --- a/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx +++ b/apps/meteor/tests/unit/client/views/room/MessageList/MessageContent.spec.tsx @@ -12,6 +12,17 @@ const baseMessage = { username: 'userName', }, msg: 'message', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message', + }, + ], + }, + ], rid: 'roomId', _id: 'messageId', _updatedAt: date, @@ -41,6 +52,7 @@ const MessageContent = proxyquire.noCallThru().load('../../../../../../client/vi useUserData: () => '', }, '../../../blocks/MessageBlock': () => '', + '../../../../components/message/Attachments': () => '', './MessageContentBody': () => baseMessage.msg, }).default; diff --git a/apps/meteor/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts similarity index 88% rename from apps/meteor/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts rename to apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts index 5e98a23a7c8..cd197854f9f 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageFirstUnread.spec.ts @@ -2,11 +2,11 @@ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { isMessageFirstUnread } from './isMessageFirstUnread'; +import { isMessageFirstUnread } from '../../../../../../../client/views/room/MessageList/lib/isMessageFirstUnread'; const date = new Date('2021-10-27T00:00:00.000Z'); -const baseMessage = { +const baseMessage: IMessage = { ts: date, u: { _id: 'userId', diff --git a/apps/meteor/client/views/room/MessageList/lib/isMessageNewDay.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts similarity index 83% rename from apps/meteor/client/views/room/MessageList/lib/isMessageNewDay.spec.ts rename to apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts index 51fb1a062d3..b36d9d49e9f 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isMessageNewDay.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts @@ -1,11 +1,12 @@ /* eslint-env mocha */ +import type { IMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { isMessageNewDay } from './isMessageNewDay'; +import { isMessageNewDay } from '../../../../../../../client/views/room/MessageList/lib/isMessageNewDay'; const date = new Date('2021-10-27T00:00:00.000Z'); -const baseMessage = { +const baseMessage: IMessage = { ts: date, u: { _id: 'userId', diff --git a/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts similarity index 94% rename from apps/meteor/client/views/room/MessageList/lib/isMessageSequential.spec.ts rename to apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts index 6658f78f186..35cffceb306 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts @@ -1,13 +1,13 @@ /* eslint-env mocha */ -import { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { isMessageSequential } from './isMessageSequential'; +import { isMessageSequential } from '../../../../../../../client/views/room/MessageList/lib/isMessageSequential'; const TIME_RANGE_IN_SECONDS = 300; const date = new Date('2021-10-27T00:00:00.000Z'); -const baseMessage = { +const baseMessage: IMessage = { ts: date, u: { _id: 'userId', diff --git a/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts similarity index 84% rename from apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts rename to apps/meteor/tests/unit/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts index 7eb1af6fd79..cc98cdce664 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isOwnUserMessage.spec.ts @@ -2,12 +2,12 @@ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { MessageTypes } from '../../../../../app/ui-utils/lib/MessageTypes'; -import { isOwnUserMessage } from './isOwnUserMessage'; +import { MessageTypes } from '../../../../../../../app/ui-utils/lib/MessageTypes'; +import { isOwnUserMessage } from '../../../../../../../client/views/room/MessageList/lib/isOwnUserMessage'; const date = new Date('2021-10-27T00:00:00.000Z'); -const baseMessage = { +const baseMessage: IMessage = { ts: date, u: { _id: 'userId', diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/isParsedMessage.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isParsedMessage.spec.ts new file mode 100644 index 00000000000..db046ba52cd --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isParsedMessage.spec.ts @@ -0,0 +1,51 @@ +import type { Root } from '@rocket.chat/message-parser'; +/* eslint-env mocha */ +import type { IMessage } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { isParsedMessage } from '../../../../../../../client/views/room/MessageList/lib/isParsedMessage'; + +const date = new Date('2021-10-27T00:00:00.000Z'); + +const baseMessage: IMessage = { + ts: date, + u: { + _id: 'userId', + name: 'userName', + username: 'userName', + }, + msg: 'message', + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message', + }, + ], + }, + ], + rid: 'roomId', + _id: 'messageId', + _updatedAt: date, + urls: [], +}; + +describe('isParsedMessage', () => { + it('should return true if the message parsed', () => { + const message: IMessage = { + ...baseMessage, + }; + + expect(isParsedMessage(message.md as Root)).to.be.true; + }); + + it('should return false if the message is not parsed', () => { + const message: IMessage = { + ...baseMessage, + }; + + expect(isParsedMessage(message.msg as string)).to.be.false; + }); +}); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts new file mode 100644 index 00000000000..35225bcb4a9 --- /dev/null +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/parseMessage.spec.ts @@ -0,0 +1,137 @@ +/* eslint-env mocha */ +import type { Options, Root } from '@rocket.chat/message-parser'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { parseMessageTextToAstMarkdown } from '../../../../../../../client/views/room/MessageList/lib/parseMessageTextToAstMarkdown'; + +const date = new Date('2021-10-27T00:00:00.000Z'); + +const parseOptions: Options = { + colors: true, + emoticons: true, + katex: { + dollarSyntax: true, + parenthesisSyntax: true, + }, +}; + +const messageParserTokenMessageWithWrongData: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message', + }, + { + type: 'BOLD', + value: [ + { + type: 'PLAIN_TEXT', + value: 'bold', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' ', + }, + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'italic', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'STRIKE', + value: [ + { + type: 'PLAIN_TEXT', + value: 'strike', + }, + ], + }, + ], + }, +]; + +const messageParserTokenMessage: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'message ', + }, + { + type: 'BOLD', + value: [ + { + type: 'PLAIN_TEXT', + value: 'bold', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' ', + }, + { + type: 'ITALIC', + value: [ + { + type: 'PLAIN_TEXT', + value: 'italic', + }, + ], + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'STRIKE', + value: [ + { + type: 'PLAIN_TEXT', + value: 'strike', + }, + ], + }, + ], + }, +]; + +const baseMessage: IMessage = { + ts: date, + u: { + _id: 'userId', + name: 'userName', + username: 'userName', + }, + msg: 'message **bold** _italic_ and ~strike~', + rid: 'roomId', + _id: 'messageId', + _updatedAt: date, + urls: [], +}; + +describe('parseMessage', () => { + it('should return md property populated if the message is parsed', () => { + expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions).md).to.deep.equal(messageParserTokenMessage); + }); + + it('should return correct parsed md property populated and fail in comparison with different Root element', () => { + expect(parseMessageTextToAstMarkdown(baseMessage, parseOptions).md).to.not.deep.equal(messageParserTokenMessageWithWrongData); + }); + + // TODO: Add more tests for each type of message and for each type of token +}); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 44bc4f48600..f207f3b67a1 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -2,7 +2,7 @@ import type Url from 'url'; import type Icons from '@rocket.chat/icons'; import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; -import type { parser } from '@rocket.chat/message-parser'; +import type { Root } from '@rocket.chat/message-parser'; import type { IRocketChatRecord } from '../IRocketChatRecord'; import type { IUser } from '../IUser'; @@ -124,7 +124,7 @@ export interface IMessage extends IRocketChatRecord { u: Required<Pick<IUser, '_id' | 'username' | 'name'>>; blocks?: MessageSurfaceLayout; alias?: string; - md?: ReturnType<typeof parser>; + md?: Root; _hidden?: boolean; imported?: boolean; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentAction.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentAction.ts index 96d1b5f0a2b..3f0c549b656 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentAction.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentAction.ts @@ -1,11 +1,14 @@ // DEPRECATED +import type { Root } from '@rocket.chat/message-parser'; + import type { MessageAttachmentBase } from './MessageAttachmentBase'; type Action = { msgId?: string; type: 'button'; text: string; + md?: Root; msg?: string; url?: string; image_url?: string; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index dd9ec2be1bf..abc13a7efdd 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -1,3 +1,5 @@ +import type { Root } from '@rocket.chat/message-parser'; + export type MessageAttachmentBase = { title?: string; @@ -5,6 +7,7 @@ export type MessageAttachmentBase = { collapsed?: boolean; description?: string; text?: string; + md?: Root; title_link?: string; title_link_download?: boolean; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 5c69c731412..725cb672054 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts @@ -1,3 +1,5 @@ +import type { Root } from '@rocket.chat/message-parser'; + import type { FieldProps } from './FieldProps'; import type { Dimensions } from './Files/Dimensions'; import type { MessageAttachmentBase } from './MessageAttachmentBase'; @@ -20,6 +22,7 @@ export type MessageAttachmentDefault = { mrkdwn_in?: Array<MarkdownFields>; pretext?: string; text?: string; + md?: Root; thumb_url?: string; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts index 971ba4ecabb..c09ac876b20 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts @@ -1,3 +1,5 @@ +import type { Root } from '@rocket.chat/message-parser'; + import type { MessageAttachmentBase } from './MessageAttachmentBase'; export type MessageQuoteAttachment = { @@ -6,6 +8,7 @@ export type MessageQuoteAttachment = { author_icon: string; message_link?: string; text: string; + md: Root; attachments?: Array<MessageQuoteAttachment>; // TODO this is cauising issues to define a model, see @ts-expect-error at apps/meteor/app/api/server/v1/channels.ts:274 } & MessageAttachmentBase; diff --git a/packages/gazzodown/src/elements/PreviewInlineElements.tsx b/packages/gazzodown/src/elements/PreviewInlineElements.tsx index a174c4c6be0..4b9e8a15935 100644 --- a/packages/gazzodown/src/elements/PreviewInlineElements.tsx +++ b/packages/gazzodown/src/elements/PreviewInlineElements.tsx @@ -8,6 +8,9 @@ import KatexErrorBoundary from '../katex/KatexErrorBoundary'; import PreviewKatexElement from '../katex/PreviewKatexElement'; import PreviewChannelMentionElement from '../mentions/PreviewChannelMentionElement'; import PreviewUserMentionElement from '../mentions/PreviewUserMentionElement'; +import BoldSpan from './BoldSpan'; +import ItalicSpan from './ItalicSpan'; +import StrikeSpan from './StrikeSpan'; type PreviewInlineElementsProps = { children: MessageParser.Inlines[]; @@ -18,9 +21,13 @@ const PreviewInlineElements = ({ children }: PreviewInlineElementsProps): ReactE {children.map((child, index) => { switch (child.type) { case 'BOLD': - case 'ITALIC': + return <BoldSpan key={index} children={child.value} />; + case 'STRIKE': - return <PreviewInlineElements key={index} children={child.value} />; + return <StrikeSpan key={index} children={child.value} />; + + case 'ITALIC': + return <ItalicSpan key={index} children={child.value} />; case 'LINK': return <PreviewInlineElements key={index} children={[child.value.label]} />; -- GitLab From 909034943986059c935a5c0a7b4cbdfbcf69dd97 Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Thu, 22 Sep 2022 11:44:49 -0600 Subject: [PATCH 057/107] Chore: Omnichannel endpoints to ts (#26829) --- apps/meteor/app/api/server/api.d.ts | 1 + .../server/rest/{agent.js => agent.ts} | 12 +- .../rest/{appearance.js => appearance.ts} | 0 .../{businessHours.js => businessHours.ts} | 4 +- .../rest/{dashboards.js => dashboards.ts} | 137 +- .../livechat/imports/server/rest/facebook.js | 100 - .../livechat/imports/server/rest/facebook.ts | 119 + .../rest/{inquiries.js => inquiries.ts} | 30 +- .../rest/{integrations.js => integrations.ts} | 0 .../server/rest/{queue.js => queue.ts} | 4 +- .../server/rest/{rooms.js => rooms.ts} | 45 +- .../server/rest/{triggers.js => triggers.ts} | 8 +- .../server/rest/{upload.js => upload.ts} | 18 +- .../server/rest/{users.js => users.ts} | 23 +- .../livechat/imports/server/rest/visitors.js | 160 -- .../livechat/imports/server/rest/visitors.ts | 156 +- apps/meteor/app/livechat/server/api.js | 16 - apps/meteor/app/livechat/server/api.ts | 15 + apps/meteor/app/livechat/server/api/rest.js | 12 - apps/meteor/app/livechat/server/api/rest.ts | 12 + .../app/livechat/server/api/v1/agent.js | 70 - .../app/livechat/server/api/v1/agent.ts | 60 + .../app/livechat/server/api/v1/config.js | 38 - .../app/livechat/server/api/v1/config.ts | 37 + .../app/livechat/server/api/v1/customField.js | 105 - .../app/livechat/server/api/v1/customField.ts | 91 + .../app/livechat/server/api/v1/message.js | 325 --- .../app/livechat/server/api/v1/message.ts | 292 +++ .../livechat/server/api/v1/offlineMessage.js | 24 - .../livechat/server/api/v1/offlineMessage.ts | 20 + .../app/livechat/server/api/v1/pageVisited.js | 30 - .../app/livechat/server/api/v1/pageVisited.ts | 22 + .../meteor/app/livechat/server/api/v1/room.js | 299 --- .../meteor/app/livechat/server/api/v1/room.ts | 295 +++ .../app/livechat/server/api/v1/transcript.js | 22 - .../app/livechat/server/api/v1/transcript.ts | 21 + .../api/v1/{transfer.js => transfer.ts} | 9 +- .../api/v1/{videoCall.js => videoCall.ts} | 62 +- .../currentChats/CurrentChatsRoute.tsx | 4 +- .../currentChats/CustomFieldsVerticalBar.tsx | 8 +- .../directory/contacts/ContactTable.tsx | 2 +- .../ComposerOmnichannelInquiry.tsx | 3 + ...anned-responses.js => canned-responses.ts} | 30 +- .../server/{index.js => index.ts} | 0 .../server/lib/canned-responses.js | 12 - .../server/api/{agents.js => agents.ts} | 56 +- .../api/{departments.js => departments.ts} | 163 +- .../api/lib/{inquiries.js => inquiries.ts} | 10 +- .../api/lib/{monitors.js => monitors.ts} | 22 +- .../ee/server/models/raw/CannedResponse.ts | 4 +- apps/meteor/package.json | 2 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 41 +- .../end-to-end/api/livechat/04-dashboards.ts | 18 +- .../end-to-end/api/livechat/09-visitors.ts | 10 +- .../end-to-end/api/livechat/11-livechat.ts | 1 + .../src/models/ICannedResponseModel.ts | 4 +- .../rest-typings/src/helpers/Deprecated.ts | 1 + packages/rest-typings/src/v1/omnichannel.ts | 2213 +++++++++++++++-- yarn.lock | 10 +- 59 files changed, 3500 insertions(+), 1808 deletions(-) rename apps/meteor/app/livechat/imports/server/rest/{agent.js => agent.ts} (63%) rename apps/meteor/app/livechat/imports/server/rest/{appearance.js => appearance.ts} (100%) rename apps/meteor/app/livechat/imports/server/rest/{businessHours.js => businessHours.ts} (77%) rename apps/meteor/app/livechat/imports/server/rest/{dashboards.js => dashboards.ts} (62%) delete mode 100644 apps/meteor/app/livechat/imports/server/rest/facebook.js create mode 100644 apps/meteor/app/livechat/imports/server/rest/facebook.ts rename apps/meteor/app/livechat/imports/server/rest/{inquiries.js => inquiries.ts} (75%) rename apps/meteor/app/livechat/imports/server/rest/{integrations.js => integrations.ts} (100%) rename apps/meteor/app/livechat/imports/server/rest/{queue.js => queue.ts} (77%) rename apps/meteor/app/livechat/imports/server/rest/{rooms.js => rooms.ts} (53%) rename apps/meteor/app/livechat/imports/server/rest/{triggers.js => triggers.ts} (86%) rename apps/meteor/app/livechat/imports/server/rest/{upload.js => upload.ts} (84%) rename apps/meteor/app/livechat/imports/server/rest/{users.js => users.ts} (92%) delete mode 100644 apps/meteor/app/livechat/imports/server/rest/visitors.js delete mode 100644 apps/meteor/app/livechat/server/api.js create mode 100644 apps/meteor/app/livechat/server/api.ts delete mode 100644 apps/meteor/app/livechat/server/api/rest.js create mode 100644 apps/meteor/app/livechat/server/api/rest.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/agent.js create mode 100644 apps/meteor/app/livechat/server/api/v1/agent.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/config.js create mode 100644 apps/meteor/app/livechat/server/api/v1/config.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/customField.js create mode 100644 apps/meteor/app/livechat/server/api/v1/customField.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/message.js create mode 100644 apps/meteor/app/livechat/server/api/v1/message.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/offlineMessage.js create mode 100644 apps/meteor/app/livechat/server/api/v1/offlineMessage.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/pageVisited.js create mode 100644 apps/meteor/app/livechat/server/api/v1/pageVisited.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/room.js create mode 100644 apps/meteor/app/livechat/server/api/v1/room.ts delete mode 100644 apps/meteor/app/livechat/server/api/v1/transcript.js create mode 100644 apps/meteor/app/livechat/server/api/v1/transcript.ts rename apps/meteor/app/livechat/server/api/v1/{transfer.js => transfer.ts} (80%) rename apps/meteor/app/livechat/server/api/v1/{videoCall.js => videoCall.ts} (62%) rename apps/meteor/ee/app/api-enterprise/server/{canned-responses.js => canned-responses.ts} (74%) rename apps/meteor/ee/app/api-enterprise/server/{index.js => index.ts} (100%) rename apps/meteor/ee/app/livechat-enterprise/server/api/{agents.js => agents.ts} (69%) rename apps/meteor/ee/app/livechat-enterprise/server/api/{departments.js => departments.ts} (67%) rename apps/meteor/ee/app/livechat-enterprise/server/api/lib/{inquiries.js => inquiries.ts} (80%) rename apps/meteor/ee/app/livechat-enterprise/server/api/lib/{monitors.js => monitors.ts} (63%) create mode 100644 packages/rest-typings/src/helpers/Deprecated.ts diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index 8ddc27f0457..20630f1acf8 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -139,6 +139,7 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio me: IUser, ): TOptions extends { authRequired: true } ? UserInfo : TOptions extends { authOrAnonRequired: true } ? UserInfo | undefined : undefined; composeRoomWithLastMessage(room: IRoom, userId: string): IRoom; + isWidget(): boolean; } & (TOptions extends { authRequired: true } ? { readonly user: IUser; diff --git a/apps/meteor/app/livechat/imports/server/rest/agent.js b/apps/meteor/app/livechat/imports/server/rest/agent.ts similarity index 63% rename from apps/meteor/app/livechat/imports/server/rest/agent.js rename to apps/meteor/app/livechat/imports/server/rest/agent.ts index e388ed1be72..65ac3cd0b67 100644 --- a/apps/meteor/app/livechat/imports/server/rest/agent.js +++ b/apps/meteor/app/livechat/imports/server/rest/agent.ts @@ -1,22 +1,14 @@ -import { Match, check } from 'meteor/check'; +import { isGETLivechatAgentsAgentIdDepartmentsParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { findAgentDepartments } from '../../../server/api/lib/agents'; API.v1.addRoute( 'livechat/agents/:agentId/departments', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatAgentsAgentIdDepartmentsParams }, { async get() { - check(this.urlParams, { - agentId: String, - }); - check(this.queryParams, { - enabledDepartmentsOnly: Match.Maybe(String), - }); - const departments = await findAgentDepartments({ - userId: this.userId, enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', agentId: this.urlParams.agentId, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.js b/apps/meteor/app/livechat/imports/server/rest/appearance.ts similarity index 100% rename from apps/meteor/app/livechat/imports/server/rest/appearance.js rename to apps/meteor/app/livechat/imports/server/rest/appearance.ts diff --git a/apps/meteor/app/livechat/imports/server/rest/businessHours.js b/apps/meteor/app/livechat/imports/server/rest/businessHours.ts similarity index 77% rename from apps/meteor/app/livechat/imports/server/rest/businessHours.js rename to apps/meteor/app/livechat/imports/server/rest/businessHours.ts index 38aeaa0cafd..fc0f6209b42 100644 --- a/apps/meteor/app/livechat/imports/server/rest/businessHours.js +++ b/apps/meteor/app/livechat/imports/server/rest/businessHours.ts @@ -1,9 +1,11 @@ +import { isGETBusinessHourParams } from '@rocket.chat/rest-typings'; + import { API } from '../../../../api/server'; import { findLivechatBusinessHour } from '../../../server/api/lib/businessHours'; API.v1.addRoute( 'livechat/business-hour', - { authRequired: true, permissionsRequired: ['view-livechat-business-hours'] }, + { authRequired: true, permissionsRequired: ['view-livechat-business-hours'], validateParams: isGETBusinessHourParams }, { async get() { const { _id, type } = this.queryParams; diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.js b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts similarity index 62% rename from apps/meteor/app/livechat/imports/server/rest/dashboards.js rename to apps/meteor/app/livechat/imports/server/rest/dashboards.ts index da4eb12fc95..49895f9d907 100644 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.js +++ b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts @@ -1,4 +1,4 @@ -import { Match, check } from 'meteor/check'; +import { isGETDashboardTotalizerParams, isGETDashboardsAgentStatusParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { @@ -16,29 +16,30 @@ import { Users } from '../../../../models/server'; API.v1.addRoute( 'livechat/analytics/dashboards/conversation-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isGETDashboardTotalizerParams, + }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const user = Users.findOneById(this.userId, { fields: { utcOffset: 1, language: 1 } }); - const totalizers = getConversationsMetrics({ start, end, departmentId, user }); + // @ts-expect-error TODO: fix this + const totalizers = getConversationsMetrics({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -46,29 +47,26 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/agents-productivity-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const user = Users.findOneById(this.userId, { fields: { utcOffset: 1, language: 1 } }); - const totalizers = getAgentsProductivityMetrics({ start, end, departmentId, user }); + // @ts-expect-error TODO: fix this + const totalizers = getAgentsProductivityMetrics({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -76,27 +74,24 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/chats-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); - const totalizers = getChatsMetrics({ start, end, departmentId }); + // @ts-expect-error TODO: fix this + const totalizers = getChatsMetrics({ start: startDate, end: endDate, departmentId }); return API.v1.success(totalizers); }, }, @@ -104,29 +99,26 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/productivity-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const user = Users.findOneById(this.userId, { fields: { utcOffset: 1, language: 1 } }); - const totalizers = getProductivityMetrics({ start, end, departmentId, user }); + // @ts-expect-error TODO: fix this + const totalizers = getProductivityMetrics({ start: startDate, end: endDate, departmentId, user }); return API.v1.success(totalizers); }, @@ -135,26 +127,24 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); - const result = findAllChatsStatus({ start, end, departmentId }); + const endDate = new Date(end); + + // @ts-expect-error TODO: fix this + const result = findAllChatsStatus({ start: startDate, end: endDate, departmentId }); return API.v1.success(result); }, @@ -163,26 +153,25 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats-per-agent', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); - const result = findAllChatMetricsByAgent({ start, end, departmentId }); + const endDate = new Date(end); + // @ts-expect-error TODO: fix this + const result = findAllChatMetricsByAgent({ start: startDate, end: endDate, departmentId }) as { + [k: string]: { open: number; closed: number; onhold: number }; + }; return API.v1.success(result); }, @@ -191,12 +180,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/charts/agents-status', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardsAgentStatusParams }, { async get() { const { departmentId } = this.requestParams(); - check(departmentId, Match.Maybe(String)); + // @ts-expect-error TODO: fix this const result = findAllAgentsStatus({ departmentId }); return API.v1.success(result); @@ -206,26 +195,26 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/charts/chats-per-department', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); - const result = findAllChatMetricsByDepartment({ start, end, departmentId }); + const endDate = new Date(end); + + // @ts-expect-error TODO: fix this + const result = findAllChatMetricsByDepartment({ start: startDate, end: endDate, departmentId }) as { + [k: string]: { open: number; closed: number }; + }; return API.v1.success(result); }, @@ -234,26 +223,24 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/charts/timings', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, { async get() { - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); - const result = findAllResponseTimeMetrics({ start, end, departmentId }); + const endDate = new Date(end); + + // @ts-expect-error TODO: fix this + const result = findAllResponseTimeMetrics({ start: startDate, end: endDate, departmentId }); return API.v1.success(result); }, diff --git a/apps/meteor/app/livechat/imports/server/rest/facebook.js b/apps/meteor/app/livechat/imports/server/rest/facebook.js deleted file mode 100644 index db8f74ea264..00000000000 --- a/apps/meteor/app/livechat/imports/server/rest/facebook.js +++ /dev/null @@ -1,100 +0,0 @@ -import crypto from 'crypto'; - -import { Random } from 'meteor/random'; -import { LivechatVisitors } from '@rocket.chat/models'; - -import { API } from '../../../../api/server'; -import { LivechatRooms } from '../../../../models/server'; -import { settings } from '../../../../settings/server'; -import { Livechat } from '../../../server/lib/Livechat'; - -/** - * @api {post} /livechat/facebook Send Facebook message - * @apiName Facebook - * @apiGroup Livechat - * - * @apiParam {String} mid Facebook message id - * @apiParam {String} page Facebook pages id - * @apiParam {String} token Facebook user's token - * @apiParam {String} first_name Facebook user's first name - * @apiParam {String} last_name Facebook user's last name - * @apiParam {String} [text] Facebook message text - * @apiParam {String} [attachments] Facebook message attachments - */ -API.v1.addRoute('livechat/facebook', { - async post() { - if (!this.bodyParams.text && !this.bodyParams.attachments) { - return { - success: false, - }; - } - - if (!this.request.headers['x-hub-signature']) { - return { - success: false, - }; - } - - if (!settings.get('Livechat_Facebook_Enabled')) { - return { - success: false, - error: 'Integration disabled', - }; - } - - // validate if request come from omni - const signature = crypto - .createHmac('sha1', settings.get('Livechat_Facebook_API_Secret')) - .update(JSON.stringify(this.request.body)) - .digest('hex'); - if (this.request.headers['x-hub-signature'] !== `sha1=${signature}`) { - return { - success: false, - error: 'Invalid signature', - }; - } - - const sendMessage = { - message: { - _id: this.bodyParams.mid, - }, - roomInfo: { - facebook: { - page: this.bodyParams.page, - }, - }, - }; - let visitor = await LivechatVisitors.getVisitorByToken(this.bodyParams.token); - if (visitor) { - const rooms = LivechatRooms.findOpenByVisitorToken(visitor.token).fetch(); - if (rooms && rooms.length > 0) { - sendMessage.message.rid = rooms[0]._id; - } else { - sendMessage.message.rid = Random.id(); - } - sendMessage.message.token = visitor.token; - } else { - sendMessage.message.rid = Random.id(); - sendMessage.message.token = this.bodyParams.token; - - const userId = await Livechat.registerGuest({ - token: sendMessage.message.token, - name: `${this.bodyParams.first_name} ${this.bodyParams.last_name}`, - }); - - visitor = await LivechatVisitors.findOneById(userId); - } - - sendMessage.message.msg = this.bodyParams.text; - sendMessage.guest = visitor; - - try { - return { - sucess: true, - message: Livechat.sendMessage(sendMessage), - }; - } catch (e) { - Livechat.logger.error('Error using Facebook ->', e); - } - }, -}); diff --git a/apps/meteor/app/livechat/imports/server/rest/facebook.ts b/apps/meteor/app/livechat/imports/server/rest/facebook.ts new file mode 100644 index 00000000000..21a68fa552c --- /dev/null +++ b/apps/meteor/app/livechat/imports/server/rest/facebook.ts @@ -0,0 +1,119 @@ +import crypto from 'crypto'; + +import { isPOSTLivechatFacebookParams } from '@rocket.chat/rest-typings'; +import { Random } from 'meteor/random'; +import { LivechatVisitors } from '@rocket.chat/models'; +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; + +import { API } from '../../../../api/server'; +import { LivechatRooms } from '../../../../models/server'; +import { settings } from '../../../../settings/server'; +import { Livechat } from '../../../server/lib/Livechat'; + +type SentMessage = { + message: { + _id: string; + rid?: string; + token?: string; + msg?: string; + }; + roomInfo: { + facebook: { + page: string; + }; + }; + guest?: ILivechatVisitor | null; +}; + +/** + * @api {post} /livechat/facebook Send Facebook message + * @apiName Facebook + * @apiGroup Livechat + * + * @apiParam {String} mid Facebook message id + * @apiParam {String} page Facebook pages id + * @apiParam {String} token Facebook user's token + * @apiParam {String} first_name Facebook user's first name + * @apiParam {String} last_name Facebook user's last name + * @apiParam {String} [text] Facebook message text + * @apiParam {String} [attachments] Facebook message attachments + */ +API.v1.addRoute( + 'livechat/facebook', + { validateParams: isPOSTLivechatFacebookParams }, + { + async post() { + if (!this.bodyParams.text && !this.bodyParams.attachments) { + return API.v1.failure('Invalid request'); + } + + if (!this.request.headers['x-hub-signature']) { + return API.v1.unauthorized(); + } + + if (!settings.get<boolean>('Livechat_Facebook_Enabled')) { + return API.v1.failure('Facebook integration is disabled'); + } + + // validate if request come from omni + const signature = crypto + .createHmac('sha1', settings.get<string>('Livechat_Facebook_API_Secret')) + .update(JSON.stringify(this.request.body)) + .digest('hex'); + if (this.request.headers['x-hub-signature'] !== `sha1=${signature}`) { + return API.v1.unauthorized(); + } + + const sendMessage: SentMessage = { + message: { + _id: this.bodyParams.mid, + msg: this.bodyParams.text, + }, + roomInfo: { + facebook: { + page: this.bodyParams.page, + }, + }, + }; + let visitor = await LivechatVisitors.getVisitorByToken(this.bodyParams.token, {}); + if (visitor) { + const rooms = LivechatRooms.findOpenByVisitorToken(visitor.token).fetch(); + if (rooms && rooms.length > 0) { + sendMessage.message.rid = rooms[0]._id; + } else { + sendMessage.message.rid = Random.id(); + } + sendMessage.message.token = visitor.token; + } else { + sendMessage.message.rid = Random.id(); + sendMessage.message.token = this.bodyParams.token; + + const userId = await Livechat.registerGuest({ + token: sendMessage.message.token, + name: `${this.bodyParams.first_name} ${this.bodyParams.last_name}`, + // TODO: type livechat big file :( + id: undefined, + email: undefined, + phone: undefined, + department: undefined, + username: undefined, + connectionData: undefined, + }); + + visitor = await LivechatVisitors.findOneById(userId); + } + + sendMessage.guest = visitor; + + try { + return API.v1.success({ + // @ts-expect-error - Typings on Livechat.sendMessage are wrong + message: await Livechat.sendMessage(sendMessage), + }); + } catch (e) { + Livechat.logger.error('Error using Facebook ->', e); + return API.v1.failure(e); + } + }, + }, +); diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.js b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts similarity index 75% rename from apps/meteor/app/livechat/imports/server/rest/inquiries.js rename to apps/meteor/app/livechat/imports/server/rest/inquiries.ts index 6cd4440ec35..1b5864076d5 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.js +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts @@ -1,5 +1,11 @@ +import { + isGETLivechatInquiriesListParams, + isPOSTLivechatInquiriesTakeParams, + isGETLivechatInquiriesQueuedParams, + isGETLivechatInquiriesQueuedForUserParams, + isGETLivechatInquiriesGetOneParams, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { LivechatInquiry } from '@rocket.chat/models'; @@ -9,19 +15,20 @@ import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/i API.v1.addRoute( 'livechat/inquiries.list', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETLivechatInquiriesListParams }, { async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); const { department } = this.requestParams(); - const ourQuery = Object.assign({}, { status: 'queued' }); + const ourQuery: { status: string; department?: string } = { status: 'queued' }; if (department) { const departmentFromDB = LivechatDepartment.findOneByIdOrName(department); if (departmentFromDB) { ourQuery.department = departmentFromDB._id; } } + // @ts-expect-error - attachments... const { cursor, totalCount } = LivechatInquiry.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, @@ -49,13 +56,9 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/inquiries.take', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPOSTLivechatInquiriesTakeParams }, { async post() { - check(this.bodyParams, { - inquiryId: String, - userId: Match.Maybe(String), - }); if (this.bodyParams.userId && !Users.findOneById(this.bodyParams.userId, { fields: { _id: 1 } })) { return API.v1.failure('The user is invalid'); } @@ -70,7 +73,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/inquiries.queued', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesQueuedParams }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -95,7 +98,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/inquiries.queuedForUser', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesQueuedForUserParams }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -105,7 +108,7 @@ API.v1.addRoute( return API.v1.success( await findInquiries({ userId: this.userId, - filterDepartment: department, + department, status: LivechatInquiryStatus.QUEUED, pagination: { offset, @@ -120,13 +123,10 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/inquiries.getOne', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesGetOneParams }, { async get() { const { roomId } = this.queryParams; - if (!roomId) { - return API.v1.failure("The 'roomId' param is required"); - } return API.v1.success( await findOneInquiryByRoomId({ diff --git a/apps/meteor/app/livechat/imports/server/rest/integrations.js b/apps/meteor/app/livechat/imports/server/rest/integrations.ts similarity index 100% rename from apps/meteor/app/livechat/imports/server/rest/integrations.js rename to apps/meteor/app/livechat/imports/server/rest/integrations.ts diff --git a/apps/meteor/app/livechat/imports/server/rest/queue.js b/apps/meteor/app/livechat/imports/server/rest/queue.ts similarity index 77% rename from apps/meteor/app/livechat/imports/server/rest/queue.js rename to apps/meteor/app/livechat/imports/server/rest/queue.ts index 0e63f3a2482..fa7843c7492 100644 --- a/apps/meteor/app/livechat/imports/server/rest/queue.js +++ b/apps/meteor/app/livechat/imports/server/rest/queue.ts @@ -1,9 +1,11 @@ +import { isGETLivechatQueueParams } from '@rocket.chat/rest-typings'; + import { API } from '../../../../api/server'; import { findQueueMetrics } from '../../../server/api/lib/queue'; API.v1.addRoute( 'livechat/queue', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatQueueParams }, { async get() { const { offset, count } = this.getPaginationItems(); diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.js b/apps/meteor/app/livechat/imports/server/rest/rooms.ts similarity index 53% rename from apps/meteor/app/livechat/imports/server/rest/rooms.js rename to apps/meteor/app/livechat/imports/server/rest/rooms.ts index c2b0e8236d5..c885a71f08c 100644 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.js +++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts @@ -1,41 +1,36 @@ -import { Match, check } from 'meteor/check'; +import { isGETLivechatRoomsParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { findRooms } from '../../../server/api/lib/rooms'; import { hasPermission } from '../../../../authorization/server'; -const validateDateParams = (property, date) => { +const validateDateParams = (property: string, date?: string) => { + let parsedDate: { start?: string; end?: string } | undefined = undefined; if (date) { - date = JSON.parse(date); + parsedDate = JSON.parse(date) as { start?: string; end?: string }; } - if (date && date.start && isNaN(Date.parse(date.start))) { + + if (parsedDate?.start && isNaN(Date.parse(parsedDate.start))) { throw new Error(`The "${property}.start" query parameter must be a valid date.`); } - if (date && date.end && isNaN(Date.parse(date.end))) { + if (parsedDate?.end && isNaN(Date.parse(parsedDate.end))) { throw new Error(`The "${property}.end" query parameter must be a valid date.`); } - return date; + return parsedDate; }; API.v1.addRoute( 'livechat/rooms', - { authRequired: true }, + { authRequired: true, validateParams: isGETLivechatRoomsParams }, { async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); const { agents, departmentId, open, tags, roomName, onhold } = this.requestParams(); - let { createdAt, customFields, closedAt } = this.requestParams(); - check(agents, Match.Maybe([String])); - check(roomName, Match.Maybe(String)); - check(departmentId, Match.Maybe(String)); - check(open, Match.Maybe(String)); - check(onhold, Match.Maybe(String)); - check(tags, Match.Maybe([String])); - check(customFields, Match.Maybe(String)); + const { createdAt, customFields, closedAt } = this.requestParams(); - createdAt = validateDateParams('createdAt', createdAt); - closedAt = validateDateParams('closedAt', closedAt); + const createdAtParam = validateDateParams('createdAt', createdAt); + const closedAtParam = validateDateParams('closedAt', closedAt); const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms'); const hasAgentAccess = hasPermission(this.userId, 'view-l-room') && agents?.includes(this.userId) && agents?.length === 1; @@ -43,12 +38,16 @@ API.v1.addRoute( return API.v1.unauthorized(); } + let parsedCf: { [key: string]: string } | undefined = undefined; if (customFields) { try { - const parsedCustomFields = JSON.parse(customFields); - check(parsedCustomFields, Object); + const parsedCustomFields = JSON.parse(customFields) as { [key: string]: string }; + if (typeof parsedCustomFields !== 'object' || Array.isArray(parsedCustomFields) || parsedCustomFields === null) { + throw new Error('Invalid custom fields'); + } + // Model's already checking for the keys, so we don't need to do it here. - customFields = parsedCustomFields; + parsedCf = parsedCustomFields; } catch (e) { throw new Error('The "customFields" query parameter must be a valid JSON.'); } @@ -60,10 +59,10 @@ API.v1.addRoute( roomName, departmentId, open: open && open === 'true', - createdAt, - closedAt, + createdAt: createdAtParam, + closedAt: closedAtParam, tags, - customFields, + customFields: parsedCf, onhold, options: { offset, count, sort, fields }, }), diff --git a/apps/meteor/app/livechat/imports/server/rest/triggers.js b/apps/meteor/app/livechat/imports/server/rest/triggers.ts similarity index 86% rename from apps/meteor/app/livechat/imports/server/rest/triggers.js rename to apps/meteor/app/livechat/imports/server/rest/triggers.ts index 0a87240c90a..ea742cb8411 100644 --- a/apps/meteor/app/livechat/imports/server/rest/triggers.js +++ b/apps/meteor/app/livechat/imports/server/rest/triggers.ts @@ -1,11 +1,11 @@ -import { check } from 'meteor/check'; +import { isGETLivechatTriggersParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { findTriggers, findTriggerById } from '../../../server/api/lib/triggers'; API.v1.addRoute( 'livechat/triggers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETLivechatTriggersParams }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -29,10 +29,6 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { async get() { - check(this.urlParams, { - _id: String, - }); - const trigger = await findTriggerById({ triggerId: this.urlParams._id, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.js b/apps/meteor/app/livechat/imports/server/rest/upload.ts similarity index 84% rename from apps/meteor/app/livechat/imports/server/rest/upload.js rename to apps/meteor/app/livechat/imports/server/rest/upload.ts index 079daec7be2..c7b8544ed10 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.js +++ b/apps/meteor/app/livechat/imports/server/rest/upload.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import filesize from 'filesize'; -import { LivechatVisitors, Settings } from '@rocket.chat/models'; +import { LivechatVisitors } from '@rocket.chat/models'; import { settings } from '../../../../settings/server'; import { LivechatRooms } from '../../../../models/server'; @@ -9,16 +9,6 @@ import { FileUpload } from '../../../../file-upload/server'; import { API } from '../../../../api/server'; import { getUploadFormData } from '../../../../api/server/lib/getUploadFormData'; -let maxFileSize; - -settings.watch('FileUpload_MaxFileSize', async function (value) { - try { - maxFileSize = parseInt(value); - } catch (e) { - maxFileSize = await Settings.findOneById('FileUpload_MaxFileSize').packageValue; - } -}); - API.v1.addRoute('livechat/upload/:rid', { async post() { if (!this.request.headers['x-visitor-token']) { @@ -26,7 +16,7 @@ API.v1.addRoute('livechat/upload/:rid', { } const visitorToken = this.request.headers['x-visitor-token']; - const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken as string, {}); if (!visitor) { return API.v1.unauthorized(); @@ -50,6 +40,8 @@ API.v1.addRoute('livechat/upload/:rid', { }); } + const maxFileSize = settings.get<number>('FileUpload_MaxFileSize') || 104857600; + // -1 maxFileSize means there is no limit if (maxFileSize > -1 && file.fileBuffer.length > maxFileSize) { return API.v1.failure({ @@ -70,7 +62,7 @@ API.v1.addRoute('livechat/upload/:rid', { const uploadedFile = fileStore.insertSync(details, file.fileBuffer); if (!uploadedFile) { - return API.v1.error('Invalid file'); + return API.v1.failure('Invalid file'); } uploadedFile.description = fields.description; diff --git a/apps/meteor/app/livechat/imports/server/rest/users.js b/apps/meteor/app/livechat/imports/server/rest/users.ts similarity index 92% rename from apps/meteor/app/livechat/imports/server/rest/users.js rename to apps/meteor/app/livechat/imports/server/rest/users.ts index 39b9931defc..e2625ecc0a4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/users.js +++ b/apps/meteor/app/livechat/imports/server/rest/users.ts @@ -1,5 +1,6 @@ import { check } from 'meteor/check'; import _ from 'underscore'; +import { isLivechatUsersManagerGETProps, isPOSTLivechatUsersTypeProps } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { Users } from '../../../../models/server'; @@ -18,6 +19,10 @@ API.v1.addRoute( }, POST: { permissions: ['view-livechat-manager'], operation: 'hasAll' }, }, + validateParams: { + GET: isLivechatUsersManagerGETProps, + POST: isPOSTLivechatUsersTypeProps, + }, }, { async get() { @@ -63,14 +68,6 @@ API.v1.addRoute( throw new Error('Invalid type'); }, async post() { - check(this.urlParams, { - type: String, - }); - - check(this.bodyParams, { - username: String, - }); - if (this.urlParams.type === 'agent') { const user = Livechat.addAgent(this.bodyParams.username); if (user) { @@ -95,11 +92,6 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { async get() { - check(this.urlParams, { - type: String, - _id: String, - }); - const user = Users.findOneById(this.urlParams._id); if (!user) { @@ -127,11 +119,6 @@ API.v1.addRoute( }); }, async delete() { - check(this.urlParams, { - type: String, - _id: String, - }); - const user = Users.findOneById(this.urlParams._id); if (!user) { diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.js b/apps/meteor/app/livechat/imports/server/rest/visitors.js deleted file mode 100644 index 1bb8fba375e..00000000000 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.js +++ /dev/null @@ -1,160 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../../../api/server'; -import { - findVisitorInfo, - findVisitedPages, - findChatHistory, - searchChats, - findVisitorsToAutocomplete, - findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField, -} from '../../../server/api/lib/visitors'; - -API.v1.addRoute( - 'livechat/visitors.info', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.queryParams, { - visitorId: String, - }); - - const visitor = await findVisitorInfo({ visitorId: this.queryParams.visitorId }); - - return API.v1.success(visitor); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.pagesVisited/:roomId', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.urlParams, { - roomId: String, - }); - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - - const pages = await findVisitedPages({ - roomId: this.urlParams.roomId, - pagination: { - offset, - count, - sort, - }, - }); - return API.v1.success(pages); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.urlParams, { - visitorId: String, - roomId: String, - }); - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - const history = await findChatHistory({ - userId: this.userId, - roomId: this.urlParams.roomId, - visitorId: this.urlParams.visitorId, - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(history); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.searchChats/room/:roomId/visitor/:visitorId', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.urlParams, { - visitorId: String, - roomId: String, - }); - const { roomId, visitorId } = this.urlParams; - const { searchText, closedChatsOnly, servedChatsOnly } = this.queryParams; - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - const history = await searchChats({ - userId: this.userId, - roomId, - visitorId, - searchText, - closedChatsOnly, - servedChatsOnly, - pagination: { - offset, - count, - sort, - }, - }); - return API.v1.success(history); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.autocomplete', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - const { selector } = this.queryParams; - if (!selector) { - return API.v1.failure("The 'selector' param is required"); - } - - return API.v1.success( - await findVisitorsToAutocomplete({ - userId: this.userId, - selector: JSON.parse(selector), - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.search', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - const { term } = this.requestParams(); - - check(term, Match.Maybe(String)); - - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - - const nameOrUsername = new RegExp(escapeRegExp(term), 'i'); - - return API.v1.success( - await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ - userId: this.userId, - emailOrPhone: term, - nameOrUsername, - pagination: { - offset, - count, - sort, - }, - }), - ); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.ts b/apps/meteor/app/livechat/imports/server/rest/visitors.ts index 134286d700c..69e99086e37 100644 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.ts +++ b/apps/meteor/app/livechat/imports/server/rest/visitors.ts @@ -1,20 +1,166 @@ -import { check } from 'meteor/check'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { + isLivechatVisitorsInfoProps, + isGETLivechatVisitorsPagesVisitedRoomIdParams, + isGETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams, + isGETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams, + isGETLivechatVisitorsAutocompleteParams, + isLivechatRidMessagesProps, + isGETLivechatVisitorsSearch, +} from '@rocket.chat/rest-typings'; import { Messages } from '@rocket.chat/models'; import { API } from '../../../../api/server'; +import { + findVisitorInfo, + findVisitedPages, + findChatHistory, + searchChats, + findVisitorsToAutocomplete, + findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField, +} from '../../../server/api/lib/visitors'; import { LivechatRooms } from '../../../../models/server'; import { normalizeMessagesForUser } from '../../../../utils/server/lib/normalizeMessagesForUser'; import { canAccessRoom } from '../../../../authorization/server'; API.v1.addRoute( - 'livechat/:rid/messages', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + 'livechat/visitors.info', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatVisitorsInfoProps }, + { + async get() { + const visitor = await findVisitorInfo({ visitorId: this.queryParams.visitorId }); + return API.v1.success(visitor); + }, + }, +); + +API.v1.addRoute( + 'livechat/visitors.pagesVisited/:roomId', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsPagesVisitedRoomIdParams }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const pages = await findVisitedPages({ + roomId: this.urlParams.roomId, + pagination: { + offset, + count, + sort, + }, + }); + return API.v1.success(pages); + }, + }, +); + +API.v1.addRoute( + 'livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId', + { + authRequired: true, + permissionsRequired: ['view-l-room'], + validateParams: isGETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams, + }, { async get() { - check(this.urlParams, { - rid: String, + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const history = await findChatHistory({ + userId: this.userId, + roomId: this.urlParams.roomId, + visitorId: this.urlParams.visitorId, + pagination: { + offset, + count, + sort, + }, }); + return API.v1.success(history); + }, + }, +); + +API.v1.addRoute( + 'livechat/visitors.searchChats/room/:roomId/visitor/:visitorId', + { + authRequired: true, + permissionsRequired: ['view-l-room'], + validateParams: isGETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams, + }, + { + async get() { + const { roomId, visitorId } = this.urlParams; + const { searchText, closedChatsOnly, servedChatsOnly } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const history = await searchChats({ + userId: this.userId, + roomId, + visitorId, + searchText, + closedChatsOnly, + servedChatsOnly, + pagination: { + offset, + count, + sort, + }, + }); + return API.v1.success(history); + }, + }, +); + +API.v1.addRoute( + 'livechat/visitors.autocomplete', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsAutocompleteParams }, + { + async get() { + const { selector } = this.queryParams; + + return API.v1.success( + await findVisitorsToAutocomplete({ + selector: JSON.parse(selector), + }), + ); + }, + }, +); + +API.v1.addRoute( + 'livechat/visitors.search', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsSearch }, + { + async get() { + const { term } = this.requestParams(); + + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const nameOrUsername = term && new RegExp(escapeRegExp(term), 'i'); + + return API.v1.success( + await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ + emailOrPhone: term, + nameOrUsername, + pagination: { + offset, + count, + sort, + }, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'livechat/:rid/messages', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRidMessagesProps }, + { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); diff --git a/apps/meteor/app/livechat/server/api.js b/apps/meteor/app/livechat/server/api.js deleted file mode 100644 index 55f2768a094..00000000000 --- a/apps/meteor/app/livechat/server/api.js +++ /dev/null @@ -1,16 +0,0 @@ -import '../imports/server/rest/agent.js'; -import '../imports/server/rest/departments'; -import '../imports/server/rest/facebook.js'; -import '../imports/server/rest/sms.js'; -import '../imports/server/rest/users.js'; -import '../imports/server/rest/upload.js'; -import '../imports/server/rest/inquiries.js'; -import '../imports/server/rest/rooms.js'; -import '../imports/server/rest/appearance.js'; -import '../imports/server/rest/triggers.js'; -import '../imports/server/rest/integrations.js'; -import '../imports/server/rest/visitors.js'; -import '../imports/server/rest/visitors.ts'; -import '../imports/server/rest/dashboards.js'; -import '../imports/server/rest/queue.js'; -import '../imports/server/rest/businessHours.js'; diff --git a/apps/meteor/app/livechat/server/api.ts b/apps/meteor/app/livechat/server/api.ts new file mode 100644 index 00000000000..eaf38ff18eb --- /dev/null +++ b/apps/meteor/app/livechat/server/api.ts @@ -0,0 +1,15 @@ +import '../imports/server/rest/agent'; +import '../imports/server/rest/departments'; +import '../imports/server/rest/facebook'; +import '../imports/server/rest/sms.js'; +import '../imports/server/rest/users'; +import '../imports/server/rest/upload'; +import '../imports/server/rest/inquiries'; +import '../imports/server/rest/rooms'; +import '../imports/server/rest/appearance'; +import '../imports/server/rest/triggers'; +import '../imports/server/rest/integrations'; +import '../imports/server/rest/visitors'; +import '../imports/server/rest/dashboards'; +import '../imports/server/rest/queue'; +import '../imports/server/rest/businessHours'; diff --git a/apps/meteor/app/livechat/server/api/rest.js b/apps/meteor/app/livechat/server/api/rest.js deleted file mode 100644 index bfc6fe85465..00000000000 --- a/apps/meteor/app/livechat/server/api/rest.js +++ /dev/null @@ -1,12 +0,0 @@ -import './v1/config.js'; -import './v1/visitor'; -import './v1/transcript.js'; -import './v1/offlineMessage.js'; -import './v1/pageVisited.js'; -import './v1/agent.js'; -import './v1/message.js'; -import './v1/customField.js'; -import './v1/room.js'; -import './v1/videoCall.js'; -import './v1/transfer.js'; -import './v1/contact'; diff --git a/apps/meteor/app/livechat/server/api/rest.ts b/apps/meteor/app/livechat/server/api/rest.ts new file mode 100644 index 00000000000..4fca3640a0b --- /dev/null +++ b/apps/meteor/app/livechat/server/api/rest.ts @@ -0,0 +1,12 @@ +import './v1/config'; +import './v1/visitor'; +import './v1/transcript'; +import './v1/offlineMessage'; +import './v1/pageVisited'; +import './v1/agent'; +import './v1/message'; +import './v1/customField'; +import './v1/room'; +import './v1/videoCall'; +import './v1/transfer'; +import './v1/contact'; diff --git a/apps/meteor/app/livechat/server/api/v1/agent.js b/apps/meteor/app/livechat/server/api/v1/agent.js deleted file mode 100644 index b08a52dcbf9..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/agent.js +++ /dev/null @@ -1,70 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../../../api/server'; -import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; -import { Livechat } from '../../lib/Livechat'; - -API.v1.addRoute('livechat/agent.info/:rid/:token', { - async get() { - check(this.urlParams, { - rid: String, - token: String, - }); - - const visitor = await findGuest(this.urlParams.token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(this.urlParams.token, this.urlParams.rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const agent = room && room.servedBy && findAgent(room.servedBy._id); - if (!agent) { - throw new Meteor.Error('invalid-agent'); - } - - return API.v1.success({ agent }); - }, -}); - -API.v1.addRoute('livechat/agent.next/:token', { - async get() { - check(this.urlParams, { - token: String, - }); - - check(this.queryParams, { - department: Match.Maybe(String), - }); - - const { token } = this.urlParams; - const room = findOpenRoom(token); - if (room) { - return API.v1.success(); - } - - let { department } = this.queryParams; - if (!department) { - const requireDeparment = Livechat.getRequiredDepartment(); - if (requireDeparment) { - department = requireDeparment._id; - } - } - - const agentData = await Livechat.getNextAgent(department); - if (!agentData) { - throw new Meteor.Error('agent-not-found'); - } - - const agent = findAgent(agentData.agentId); - if (!agent) { - throw new Meteor.Error('invalid-agent'); - } - - return API.v1.success({ agent }); - }, -}); diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts new file mode 100644 index 00000000000..d1a3d73894f --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -0,0 +1,60 @@ +import { isGETAgentNextToken } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute('livechat/agent.info/:rid/:token', { + async get() { + const visitor = await findGuest(this.urlParams.token); + if (!visitor) { + throw new Error('invalid-token'); + } + + const room = findRoom(this.urlParams.token, this.urlParams.rid); + if (!room) { + throw new Error('invalid-room'); + } + + const agent = room?.servedBy && findAgent(room.servedBy._id); + if (!agent) { + throw new Error('invalid-agent'); + } + + return API.v1.success({ agent }); + }, +}); + +API.v1.addRoute( + 'livechat/agent.next/:token', + { validateParams: isGETAgentNextToken }, + { + async get() { + const { token } = this.urlParams; + const room = findOpenRoom(token); + if (room) { + return API.v1.success(); + } + + let { department } = this.queryParams; + if (!department) { + const requireDeparment = Livechat.getRequiredDepartment(); + if (requireDeparment) { + department = requireDeparment._id; + } + } + + const agentData = await Livechat.getNextAgent(department); + if (!agentData) { + throw new Error('agent-not-found'); + } + + const agent = findAgent(agentData.agentId); + if (!agent) { + throw new Error('invalid-agent'); + } + + return API.v1.success({ agent }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/config.js b/apps/meteor/app/livechat/server/api/v1/config.js deleted file mode 100644 index 3790fd3f2bd..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/config.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Match, check } from 'meteor/check'; -import mem from 'mem'; - -import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; -import { settings, findOpenRoom, getExtraConfigInfo, findAgent } from '../lib/livechat'; - -const cachedSettings = mem(settings, { maxAge: 1000, cacheKey: JSON.stringify }); - -API.v1.addRoute('livechat/config', { - async get() { - check(this.queryParams, { - token: Match.Maybe(String), - department: Match.Maybe(String), - businessUnit: Match.Maybe(String), - }); - const enabled = Livechat.enabled(); - - if (!enabled) { - return API.v1.success({ config: { enabled: false } }); - } - - const { token, department, businessUnit } = this.queryParams; - - const config = await cachedSettings({ businessUnit }); - - const status = Livechat.online(department); - const guest = token && (await Livechat.findGuest(token)); - - const room = guest && findOpenRoom(token); - const agent = guest && room && room.servedBy && findAgent(room.servedBy._id); - - const extra = await getExtraConfigInfo(room); - return API.v1.success({ - config: { ...config, online: status, guest, room, agent, ...extra }, - }); - }, -}); diff --git a/apps/meteor/app/livechat/server/api/v1/config.ts b/apps/meteor/app/livechat/server/api/v1/config.ts new file mode 100644 index 00000000000..be95fd61cc2 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/config.ts @@ -0,0 +1,37 @@ +import mem from 'mem'; +import { isGETLivechatConfigParams } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { Livechat } from '../../lib/Livechat'; +import { settings, findOpenRoom, getExtraConfigInfo, findAgent } from '../lib/livechat'; + +const cachedSettings = mem(settings, { maxAge: 1000, cacheKey: JSON.stringify }); + +API.v1.addRoute( + 'livechat/config', + { validateParams: isGETLivechatConfigParams }, + { + async get() { + const enabled = Livechat.enabled(); + + if (!enabled) { + return API.v1.success({ config: { enabled: false } }); + } + + const { token, department, businessUnit } = this.queryParams; + + const config = await cachedSettings({ businessUnit }); + + const status = Livechat.online(department); + const guest = token && (await Livechat.findGuest(token)); + + const room = guest && findOpenRoom(token); + const agent = guest && room && room.servedBy && findAgent(room.servedBy._id); + + const extra = await getExtraConfigInfo(room); + return API.v1.success({ + config: { ...config, online: status, ...extra, ...(guest && { guest }), ...(room && { room }), ...(agent && { agent }) }, + }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/customField.js b/apps/meteor/app/livechat/server/api/v1/customField.js deleted file mode 100644 index 9fae84e4372..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/customField.js +++ /dev/null @@ -1,105 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../../../api/server'; -import { findGuest } from '../lib/livechat'; -import { Livechat } from '../../lib/Livechat'; -import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFields'; - -API.v1.addRoute('livechat/custom.field', { - async post() { - check(this.bodyParams, { - token: String, - key: String, - value: String, - overwrite: Boolean, - }); - - const { token, key, value, overwrite } = this.bodyParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { - return API.v1.failure(); - } - - return API.v1.success({ field: { key, value, overwrite } }); - }, -}); - -API.v1.addRoute('livechat/custom.fields', { - async post() { - check(this.bodyParams, { - token: String, - customFields: [ - Match.ObjectIncluding({ - key: String, - value: String, - overwrite: Boolean, - }), - ], - }); - - const { token } = this.bodyParams; - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const fields = await Promise.all( - this.bodyParams.customFields.map(async (customField) => { - const data = Object.assign({ token }, customField); - if (!(await Livechat.setCustomFields(data))) { - return API.v1.failure(); - } - - return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; - }), - ); - - return API.v1.success({ fields }); - }, -}); - -API.v1.addRoute( - 'livechat/custom-fields', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - const { text } = this.queryParams; - - const customFields = await findLivechatCustomFields({ - text, - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(customFields); - }, - }, -); - -API.v1.addRoute( - 'livechat/custom-fields/:_id', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.urlParams, { - _id: String, - }); - const { customField } = await findCustomFieldById({ customFieldId: this.urlParams._id }); - - return API.v1.success({ - customField, - }); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/customField.ts b/apps/meteor/app/livechat/server/api/v1/customField.ts new file mode 100644 index 00000000000..63e2ba371e2 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/customField.ts @@ -0,0 +1,91 @@ +import { isLivechatCustomFieldsProps, isPOSTLivechatCustomFieldParams, isPOSTLivechatCustomFieldsParams } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { findGuest } from '../lib/livechat'; +import { Livechat } from '../../lib/Livechat'; +import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFields'; + +API.v1.addRoute( + 'livechat/custom.field', + { validateParams: isPOSTLivechatCustomFieldParams }, + { + async post() { + const { token, key, value, overwrite } = this.bodyParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { + return API.v1.failure(); + } + + return API.v1.success({ field: { key, value, overwrite } }); + }, + }, +); + +API.v1.addRoute( + 'livechat/custom.fields', + { validateParams: isPOSTLivechatCustomFieldsParams }, + { + async post() { + const { token } = this.bodyParams; + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const fields = await Promise.all( + this.bodyParams.customFields.map(async (customField) => { + const data = Object.assign({ token }, customField); + if (!(await Livechat.setCustomFields(data))) { + throw new Error('error-setting-custom-field'); + } + + return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; + }), + ); + + return API.v1.success({ fields }); + }, + }, +); + +API.v1.addRoute( + 'livechat/custom-fields', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatCustomFieldsProps }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const { text } = this.queryParams; + + const customFields = await findLivechatCustomFields({ + text, + pagination: { + offset, + count, + sort, + }, + }); + + return API.v1.success(customFields); + }, + }, +); + +API.v1.addRoute( + 'livechat/custom-fields/:_id', + { authRequired: true, permissionsRequired: ['view-l-room'] }, + { + async get() { + const { customField } = await findCustomFieldById({ customFieldId: this.urlParams._id }); + + return API.v1.success({ + customField, + }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/message.js b/apps/meteor/app/livechat/server/api/v1/message.js deleted file mode 100644 index e33135674a7..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/message.js +++ /dev/null @@ -1,325 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; -import { Random } from 'meteor/random'; -import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors } from '@rocket.chat/models'; - -import { Messages, LivechatRooms } from '../../../../models/server'; -import { API } from '../../../../api/server'; -import { loadMessageHistory } from '../../../../lib'; -import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; -import { Livechat } from '../../lib/Livechat'; -import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; -import { settings } from '../../../../settings/server'; - -API.v1.addRoute('livechat/message', { - async post() { - check(this.bodyParams, { - _id: Match.Maybe(String), - token: String, - rid: String, - msg: String, - agent: Match.Maybe({ - agentId: String, - username: String, - }), - }); - - const { token, rid, agent, msg } = this.bodyParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!room.open) { - throw new Meteor.Error('room-closed'); - } - - if ( - settings.get('Livechat_enable_message_character_limit') && - msg.length > parseInt(settings.get('Livechat_message_character_limit')) - ) { - throw new Meteor.Error('message-length-exceeds-character-limit'); - } - - const _id = this.bodyParams._id || Random.id(); - - const sendMessage = { - guest, - message: { - _id, - rid, - msg, - token, - }, - agent, - roomInfo: { - source: { - type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, - }, - }; - - const result = await Livechat.sendMessage(sendMessage); - if (result) { - const message = Messages.findOneById(_id); - return API.v1.success({ message }); - } - - return API.v1.failure(); - }, -}); - -API.v1.addRoute('livechat/message/:_id', { - async get() { - check(this.urlParams, { - _id: String, - }); - - check(this.queryParams, { - token: String, - rid: String, - }); - - const { token, rid } = this.queryParams; - const { _id } = this.urlParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - let message = Messages.findOneById(_id); - if (!message) { - throw new Meteor.Error('invalid-message'); - } - - if (message.file) { - message = await normalizeMessageFileUpload(message); - } - - return API.v1.success({ message }); - }, - - async put() { - check(this.urlParams, { - _id: String, - }); - - check(this.bodyParams, { - token: String, - rid: String, - msg: String, - }); - - const { token, rid } = this.bodyParams; - const { _id } = this.urlParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const msg = Messages.findOneById(_id); - if (!msg) { - throw new Meteor.Error('invalid-message'); - } - - const result = Livechat.updateMessage({ - guest, - message: { _id: msg._id, msg: this.bodyParams.msg }, - }); - if (result) { - let message = Messages.findOneById(_id); - if (message.file) { - message = await normalizeMessageFileUpload(message); - } - - return API.v1.success({ message }); - } - - return API.v1.failure(); - }, - async delete() { - check(this.urlParams, { - _id: String, - }); - - check(this.bodyParams, { - token: String, - rid: String, - }); - - const { token, rid } = this.bodyParams; - const { _id } = this.urlParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const message = Messages.findOneById(_id); - if (!message) { - throw new Meteor.Error('invalid-message'); - } - - const result = await Livechat.deleteMessage({ guest, message }); - if (result) { - return API.v1.success({ - message: { - _id, - ts: new Date().toISOString(), - }, - }); - } - - return API.v1.failure(); - }, -}); - -API.v1.addRoute('livechat/messages.history/:rid', { - async get() { - check(this.urlParams, { - rid: String, - }); - - const { offset } = this.getPaginationItems(); - const { searchText: text, token } = this.queryParams; - const { rid } = this.urlParams; - const { sort } = this.parseJsonQuery(); - - if (!token) { - throw new Meteor.Error('error-token-param-not-provided', 'The required "token" query param is missing.'); - } - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - let ls = undefined; - if (this.queryParams.ls) { - ls = new Date(this.queryParams.ls); - } - - let end = undefined; - if (this.queryParams.end) { - end = new Date(this.queryParams.end); - } - - let limit = 20; - if (this.queryParams.limit) { - limit = parseInt(this.queryParams.limit); - } - - const messages = loadMessageHistory({ - userId: guest._id, - rid, - end, - limit, - ls, - sort, - offset, - text, - }).messages.map((...args) => Promise.await(normalizeMessageFileUpload(...args))); - return API.v1.success({ messages }); - }, -}); - -API.v1.addRoute( - 'livechat/messages', - { authRequired: true }, - { - async post() { - if (!this.bodyParams.visitor) { - return API.v1.failure('Body param "visitor" is required'); - } - if (!this.bodyParams.visitor.token) { - return API.v1.failure('Body param "visitor.token" is required'); - } - if (!this.bodyParams.messages) { - return API.v1.failure('Body param "messages" is required'); - } - if (!(this.bodyParams.messages instanceof Array)) { - return API.v1.failure('Body param "messages" is not an array'); - } - if (this.bodyParams.messages.length === 0) { - return API.v1.failure('Body param "messages" is empty'); - } - - const visitorToken = this.bodyParams.visitor.token; - - let visitor = await LivechatVisitors.getVisitorByToken(visitorToken); - let rid; - if (visitor) { - const rooms = LivechatRooms.findOpenByVisitorToken(visitorToken).fetch(); - if (rooms && rooms.length > 0) { - rid = rooms[0]._id; - } else { - rid = Random.id(); - } - } else { - rid = Random.id(); - - const guest = this.bodyParams.visitor; - guest.connectionData = normalizeHttpHeaderData(this.request.headers); - - const visitorId = await Livechat.registerGuest(guest); - visitor = await LivechatVisitors.findOneById(visitorId); - } - - const sentMessages = this.bodyParams.messages.map((message) => { - const sendMessage = { - guest: visitor, - message: { - _id: Random.id(), - rid, - token: visitorToken, - msg: message.msg, - }, - roomInfo: { - source: { - type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, - }, - }; - const sentMessage = Promise.await(Livechat.sendMessage(sendMessage)); - return { - username: sentMessage.u.username, - msg: sentMessage.msg, - ts: sentMessage.ts, - }; - }); - - return API.v1.success({ - messages: sentMessages, - }); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts new file mode 100644 index 00000000000..2c087d167b9 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -0,0 +1,292 @@ +import { Random } from 'meteor/random'; +import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { + isPOSTLivechatMessageParams, + isGETLivechatMessageIdParams, + isPUTLivechatMessageIdParams, + isDELETELivechatMessageIdParams, + isGETLivechatMessagesHistoryRidParams, + isGETLivechatMessagesParams, +} from '@rocket.chat/rest-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { Messages, LivechatRooms } from '../../../../models/server'; +import { API } from '../../../../api/server'; +import { loadMessageHistory } from '../../../../lib/server'; +import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; +import { Livechat } from '../../lib/Livechat'; +import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; +import { settings } from '../../../../settings/server'; + +API.v1.addRoute( + 'livechat/message', + { validateParams: isPOSTLivechatMessageParams }, + { + async post() { + const { token, rid, agent, msg } = this.bodyParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + if (!room.open) { + throw new Error('room-closed'); + } + + if ( + settings.get('Livechat_enable_message_character_limit') && + msg.length > parseInt(settings.get('Livechat_message_character_limit')) + ) { + throw new Error('message-length-exceeds-character-limit'); + } + + const _id = this.bodyParams._id || Random.id(); + + const sendMessage = { + guest, + message: { + _id, + rid, + msg, + token, + }, + agent, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }, + }; + + const result = await Livechat.sendMessage(sendMessage); + if (result) { + const message = Messages.findOneById(_id); + return API.v1.success({ message }); + } + + return API.v1.failure(); + }, + }, +); + +API.v1.addRoute( + 'livechat/message/:_id', + { validateParams: { GET: isGETLivechatMessageIdParams, PUT: isPUTLivechatMessageIdParams, DELETE: isDELETELivechatMessageIdParams } }, + { + async get() { + const { token, rid } = this.queryParams; + const { _id } = this.urlParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + let message = Messages.findOneById(_id); + if (!message) { + throw new Error('invalid-message'); + } + + if (message.file) { + message = await normalizeMessageFileUpload(message); + } + + return API.v1.success({ message }); + }, + + async put() { + const { token, rid } = this.bodyParams; + const { _id } = this.urlParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + const msg = Messages.findOneById(_id); + if (!msg) { + throw new Error('invalid-message'); + } + + const result = Livechat.updateMessage({ + guest, + message: { _id: msg._id, msg: this.bodyParams.msg }, + }); + if (result) { + let message = Messages.findOneById(_id); + if (message.file) { + message = await normalizeMessageFileUpload(message); + } + + return API.v1.success({ message }); + } + + return API.v1.failure(); + }, + async delete() { + const { token, rid } = this.bodyParams; + const { _id } = this.urlParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + const message = Messages.findOneById(_id); + if (!message) { + throw new Error('invalid-message'); + } + + const result = await Livechat.deleteMessage({ guest, message }); + if (result) { + return API.v1.success({ + message: { + _id, + ts: new Date().toISOString(), + }, + }); + } + + return API.v1.failure(); + }, + }, +); + +API.v1.addRoute( + 'livechat/messages.history/:rid', + { validateParams: isGETLivechatMessagesHistoryRidParams }, + { + async get() { + const { offset } = this.getPaginationItems(); + const { searchText: text, token } = this.queryParams; + const { rid } = this.urlParams; + const { sort } = this.parseJsonQuery(); + + if (!token) { + throw new Error('error-token-param-not-provided'); + } + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + let ls = undefined; + if (this.queryParams.ls) { + ls = new Date(this.queryParams.ls); + } + + let end = undefined; + if (this.queryParams.end) { + end = new Date(this.queryParams.end); + } + + let limit = 20; + if (this.queryParams.limit) { + limit = parseInt(`${this.queryParams.limit}`, 10); + } + + const messages = await Promise.all( + loadMessageHistory({ + userId: guest._id, + rid, + // @ts-expect-error -- typings on loadMessageHistory are wrong + end, + limit, + // @ts-expect-error -- typings on loadMessageHistory are wrong + ls, + sort, + offset, + text, + }).messages.map((message) => normalizeMessageFileUpload(message)), + ); + return API.v1.success({ messages }); + }, + }, +); + +API.v1.addRoute( + 'livechat/messages', + { authRequired: true, validateParams: isGETLivechatMessagesParams }, + { + async post() { + const visitorToken = this.bodyParams.visitor.token; + + let visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {}); + let rid: string; + if (visitor) { + const rooms = LivechatRooms.findOpenByVisitorToken(visitorToken).fetch(); + if (rooms && rooms.length > 0) { + rid = rooms[0]._id; + } else { + rid = Random.id(); + } + } else { + rid = Random.id(); + + const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor; + guest.connectionData = normalizeHttpHeaderData(this.request.headers); + + // @ts-expect-error -- Typings on registerGuest are wrong + const visitorId = await Livechat.registerGuest(guest); + visitor = await LivechatVisitors.findOneById(visitorId); + } + + const sentMessages = await Promise.all( + this.bodyParams.messages.map(async (message) => { + const sendMessage = { + guest: visitor, + message: { + _id: Random.id(), + rid, + token: visitorToken, + msg: message.msg, + }, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }, + }; + // @ts-expect-error -- Typings on sendMessage are wrong + const sentMessage = await Livechat.sendMessage(sendMessage); + return { + username: sentMessage.u.username, + msg: sentMessage.msg, + ts: sentMessage.ts, + }; + }), + ); + + return API.v1.success({ + messages: sentMessages, + }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.js b/apps/meteor/app/livechat/server/api/v1/offlineMessage.js deleted file mode 100644 index 31a5bd8b4d6..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Match, check } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; - -API.v1.addRoute('livechat/offline.message', { - async post() { - check(this.bodyParams, { - name: String, - email: String, - message: String, - department: Match.Maybe(String), - host: Match.Maybe(String), - }); - - const { name, email, message, department, host } = this.bodyParams; - if (!Livechat.sendOfflineMessage({ name, email, message, department, host })) { - return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_offline_message') }); - } - - return API.v1.success({ message: TAPi18n.__('Livechat_offline_message_sent') }); - }, -}); diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts new file mode 100644 index 00000000000..6fee3e7fc1e --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -0,0 +1,20 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute( + 'livechat/offline.message', + { validateParams: isPOSTLivechatOfflineMessageParams }, + { + async post() { + const { name, email, message, department, host } = this.bodyParams; + if (!Livechat.sendOfflineMessage({ name, email, message, department, host })) { + return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_offline_message') }); + } + + return API.v1.success({ message: TAPi18n.__('Livechat_offline_message_sent') }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.js b/apps/meteor/app/livechat/server/api/v1/pageVisited.js deleted file mode 100644 index 29ec623af40..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/pageVisited.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Match, check } from 'meteor/check'; -import _ from 'underscore'; - -import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; - -API.v1.addRoute('livechat/page.visited', { - async post() { - check(this.bodyParams, { - token: String, - rid: Match.Maybe(String), - pageInfo: Match.ObjectIncluding({ - change: String, - title: String, - location: Match.ObjectIncluding({ - href: String, - }), - }), - }); - - const { token, rid, pageInfo } = this.bodyParams; - const obj = Livechat.savePageHistory(token, rid, pageInfo); - if (obj) { - const page = _.pick(obj, 'msg', 'navigation'); - return API.v1.success({ page }); - } - - return API.v1.success(); - }, -}); diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts new file mode 100644 index 00000000000..10c1fba01e3 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts @@ -0,0 +1,22 @@ +import { isPOSTLivechatPageVisitedParams } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute( + 'livechat/page.visited', + { validateParams: isPOSTLivechatPageVisitedParams }, + { + async post() { + const { token, rid, pageInfo } = this.bodyParams; + const obj = Livechat.savePageHistory(token, rid, pageInfo); + if (obj) { + // @ts-expect-error -- typings on savePageHistory are wrong + const { msg, navigation } = obj; + return API.v1.success({ page: { msg, navigation } }); + } + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js deleted file mode 100644 index 95825c4bfd6..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ /dev/null @@ -1,299 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; -import { Random } from 'meteor/random'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Users } from '@rocket.chat/models'; -import { isLiveChatRoomForwardProps } from '@rocket.chat/rest-typings'; - -import { settings as rcSettings } from '../../../../settings/server'; -import { Messages, LivechatRooms } from '../../../../models/server'; -import { API } from '../../../../api/server'; -import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; -import { Livechat } from '../../lib/Livechat'; -import { normalizeTransferredByData } from '../../lib/Helper'; -import { findVisitorInfo } from '../lib/visitors'; -import { canAccessRoom } from '../../../../authorization/server'; -import { addUserToRoom } from '../../../../lib/server/functions'; -import { apiDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; -import { deprecationWarning } from '../../../../api/server/helpers/deprecationWarning'; - -API.v1.addRoute('livechat/room', { - async get() { - const defaultCheckParams = { - token: String, - rid: Match.Maybe(String), - agentId: Match.Maybe(String), - }; - - const extraCheckParams = onCheckRoomParams(defaultCheckParams); - - check(this.queryParams, extraCheckParams); - - const { token, rid: roomId, agentId, ...extraParams } = this.queryParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - let room; - if (!roomId) { - room = LivechatRooms.findOneOpenByVisitorToken(token, {}); - if (room) { - return API.v1.success({ room, newRoom: false }); - } - - let agent; - const agentObj = agentId && findAgent(agentId); - if (agentObj) { - const { username } = agentObj; - agent = { agentId, username }; - } - - const rid = Random.id(); - const roomInfo = { - source: { - type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, - }; - - room = await getRoom({ guest, rid, agent, roomInfo, extraParams }); - return API.v1.success(room); - } - - room = LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token, {}); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - return API.v1.success({ room, newRoom: false }); - }, -}); - -API.v1.addRoute('livechat/room.close', { - async post() { - check(this.bodyParams, { - rid: String, - token: String, - }); - - const { rid, token } = this.bodyParams; - - const visitor = await findGuest(token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!room.open) { - throw new Meteor.Error('room-closed'); - } - - const language = rcSettings.get('Language') || 'en'; - const comment = TAPi18n.__('Closed_by_visitor', { lng: language }); - - if (!Livechat.closeRoom({ visitor, room, comment })) { - return API.v1.failure(); - } - - return API.v1.success({ rid, comment }); - }, -}); - -API.v1.addRoute('livechat/room.transfer', { - async post() { - apiDeprecationLogger.warn('livechat/room.transfer has been deprecated. Use livechat/room.forward instead.'); - check(this.bodyParams, { - rid: String, - token: String, - department: String, - }); - - const { rid, token, department } = this.bodyParams; - - const guest = await findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - let room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - // update visited page history to not expire - Messages.keepHistoryForToken(token); - - const { _id, username, name } = guest; - const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - - if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { - return API.v1.failure(); - } - - room = findRoom(token, rid); - return API.v1.success( - deprecationWarning({ - endpoint: 'livechat/room.transfer', - versionWillBeRemoved: '6.0', - response: { room }, - }), - ); - }, -}); - -API.v1.addRoute('livechat/room.survey', { - async post() { - check(this.bodyParams, { - rid: String, - token: String, - data: [ - Match.ObjectIncluding({ - name: String, - value: String, - }), - ], - }); - - const { rid, token, data } = this.bodyParams; - - const visitor = await findGuest(token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const config = await settings(); - if (!config.survey || !config.survey.items || !config.survey.values) { - throw new Meteor.Error('invalid-livechat-config'); - } - - const updateData = {}; - for (const item of data) { - if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') { - updateData[item.name] = item.value; - } - } - - if (Object.keys(updateData).length === 0) { - throw new Meteor.Error('invalid-data'); - } - - if (!LivechatRooms.updateSurveyFeedbackById(room._id, updateData)) { - return API.v1.failure(); - } - - return API.v1.success({ rid, data: updateData }); - }, -}); - -API.v1.addRoute( - 'livechat/room.forward', - { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, - { - async post() { - const transferData = this.bodyParams; - - const room = await LivechatRooms.findOneById(this.bodyParams.roomId); - if (!room || room.t !== 'l') { - throw new Error('error-invalid-room', 'Invalid room'); - } - - if (!room.open) { - throw new Error('This_conversation_is_already_closed'); - } - - const guest = await LivechatVisitors.findOneById(room.v && room.v._id); - transferData.transferredBy = normalizeTransferredByData(Meteor.user() || {}, room); - if (transferData.userId) { - const userToTransfer = await Users.findOneById(transferData.userId); - transferData.transferredTo = { - _id: userToTransfer._id, - username: userToTransfer.username, - name: userToTransfer.name, - }; - } - - const chatForwardedResult = await Livechat.transfer(room, guest, transferData); - - return chatForwardedResult ? API.v1.success() : API.v1.failure(); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.visitor', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async put() { - // This endpoint is deprecated and will be removed in future versions. - check(this.bodyParams, { - rid: String, - oldVisitorId: String, - newVisitorId: String, - }); - - const { rid, newVisitorId, oldVisitorId } = this.bodyParams; - - const { visitor } = await findVisitorInfo({ userId: this.userId, visitorId: newVisitorId }); - if (!visitor) { - throw new Meteor.Error('invalid-visitor'); - } - - let room = LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - const { v: { _id: roomVisitorId } = {} } = room; // TODO: v it will be undefined - if (roomVisitorId !== oldVisitorId) { - throw new Meteor.Error('invalid-room-visitor'); - } - - room = Livechat.changeRoomVisitor(this.userId, rid, visitor); - - return API.v1.success({ room }); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.join', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - check(this.queryParams, { roomId: String }); - - const { roomId } = this.queryParams; - - const { user } = this; - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'joinRoom' }); - } - - const room = LivechatRooms.findOneById(roomId); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'joinRoom' }); - } - - if (!canAccessRoom(room, user)) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } - - addUserToRoom(roomId, user); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts new file mode 100644 index 00000000000..6bb4208f2a0 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -0,0 +1,295 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { Random } from 'meteor/random'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors, Users } from '@rocket.chat/models'; +import { + isLiveChatRoomForwardProps, + isPOSTLivechatRoomCloseParams, + isPOSTLivechatRoomTransferParams, + isPOSTLivechatRoomSurveyParams, + isLiveChatRoomJoinProps, + isPUTLivechatRoomVisitorParams, +} from '@rocket.chat/rest-typings'; + +import { settings as rcSettings } from '../../../../settings/server'; +import { Messages, LivechatRooms } from '../../../../models/server'; +import { API } from '../../../../api/server'; +import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; +import { Livechat } from '../../lib/Livechat'; +import { normalizeTransferredByData } from '../../lib/Helper'; +import { findVisitorInfo } from '../lib/visitors'; +import { canAccessRoom } from '../../../../authorization/server'; +import { addUserToRoom } from '../../../../lib/server/functions'; +import { apiDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; +import { deprecationWarning } from '../../../../api/server/helpers/deprecationWarning'; + +API.v1.addRoute('livechat/room', { + async get() { + // I'll temporary use check for validation, as validateParams doesnt support what's being done here + const extraCheckParams = onCheckRoomParams({ + token: String, + rid: Match.Maybe(String), + agentId: Match.Maybe(String), + }); + + check(this.queryParams, extraCheckParams); + + const { token, rid: roomId, agentId, ...extraParams } = this.queryParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + let room: IOmnichannelRoom; + if (!roomId) { + room = LivechatRooms.findOneOpenByVisitorToken(token, {}); + if (room) { + return API.v1.success({ room, newRoom: false }); + } + + let agent; + const agentObj = agentId && findAgent(agentId); + if (agentObj) { + const { username } = agentObj; + agent = { agentId, username }; + } + + const rid = Random.id(); + const roomInfo = { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }; + + const newRoom = await getRoom({ guest, rid, agent, roomInfo, extraParams }); + return API.v1.success(newRoom); + } + + room = LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token, {}); + if (!room) { + throw new Error('invalid-room'); + } + + return API.v1.success({ room, newRoom: false }); + }, +}); + +API.v1.addRoute( + 'livechat/room.close', + { validateParams: isPOSTLivechatRoomCloseParams }, + { + async post() { + const { rid, token } = this.bodyParams; + + const visitor = await findGuest(token); + if (!visitor) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + if (!room.open) { + throw new Error('room-closed'); + } + + const language = rcSettings.get<string>('Language') || 'en'; + const comment = TAPi18n.__('Closed_by_visitor', { lng: language }); + + // @ts-expect-error -- typings on closeRoom are wrong + if (!Livechat.closeRoom({ visitor, room, comment })) { + return API.v1.failure(); + } + + return API.v1.success({ rid, comment }); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.transfer', + { validateParams: isPOSTLivechatRoomTransferParams }, + { + async post() { + apiDeprecationLogger.warn('livechat/room.transfer has been deprecated. Use livechat/room.forward instead.'); + + const { rid, token, department } = this.bodyParams; + + const guest = await findGuest(token); + if (!guest) { + throw new Error('invalid-token'); + } + + let room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + // update visited page history to not expire + Messages.keepHistoryForToken(token); + + const { _id, username, name } = guest; + const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); + + if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { + return API.v1.failure(); + } + + room = findRoom(token, rid); + return API.v1.success( + deprecationWarning({ + endpoint: 'livechat/room.transfer', + versionWillBeRemoved: '6.0', + response: { room }, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.survey', + { validateParams: isPOSTLivechatRoomSurveyParams }, + { + async post() { + const { rid, token, data } = this.bodyParams; + + const visitor = await findGuest(token); + if (!visitor) { + throw new Error('invalid-token'); + } + + const room = findRoom(token, rid); + if (!room) { + throw new Error('invalid-room'); + } + + const config = await settings(); + if (!config.survey || !config.survey.items || !config.survey.values) { + throw new Error('invalid-livechat-config'); + } + + const updateData: { [k: string]: string } = {}; + for (const item of data) { + if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') { + updateData[item.name] = item.value; + } + } + + if (Object.keys(updateData).length === 0) { + throw new Error('invalid-data'); + } + + if (!LivechatRooms.updateSurveyFeedbackById(room._id, updateData)) { + return API.v1.failure(); + } + + return API.v1.success({ rid, data: updateData }); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.forward', + { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, + { + async post() { + const transferData: typeof this.bodyParams & { + transferredBy?: unknown; + transferredTo?: { _id: string; username?: string; name?: string }; + } = this.bodyParams; + + const room = await LivechatRooms.findOneById(this.bodyParams.roomId); + if (!room || room.t !== 'l') { + throw new Error('error-invalid-room'); + } + + if (!room.open) { + throw new Error('This_conversation_is_already_closed'); + } + + const guest = await LivechatVisitors.findOneById(room.v?._id); + transferData.transferredBy = normalizeTransferredByData(Meteor.user() || {}, room); + if (transferData.userId) { + const userToTransfer = await Users.findOneById(transferData.userId); + if (userToTransfer) { + transferData.transferredTo = { + _id: userToTransfer._id, + username: userToTransfer.username, + name: userToTransfer.name, + }; + } + } + + const chatForwardedResult = await Livechat.transfer(room, guest, transferData); + + return chatForwardedResult ? API.v1.success() : API.v1.failure(); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.visitor', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTLivechatRoomVisitorParams }, + { + async put() { + // This endpoint is deprecated and will be removed in future versions. + const { rid, newVisitorId, oldVisitorId } = this.bodyParams; + + const { visitor } = await findVisitorInfo({ visitorId: newVisitorId }); + if (!visitor) { + throw new Error('invalid-visitor'); + } + + let room = LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + if (!room) { + throw new Error('invalid-room'); + } + + const { v: { _id: roomVisitorId = undefined } = {} } = room; // TODO: v it will be undefined + if (roomVisitorId !== oldVisitorId) { + throw new Error('invalid-room-visitor'); + } + + room = Livechat.changeRoomVisitor(this.userId, rid, visitor); + + return API.v1.success(deprecationWarning({ endpoint: 'livechat/room.visitor', versionWillBeRemoved: '6.0', response: { room } })); + }, + }, +); + +API.v1.addRoute( + 'livechat/room.join', + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomJoinProps }, + { + async get() { + const { roomId } = this.queryParams; + + const { user } = this; + + if (!user) { + throw new Error('error-invalid-user'); + } + + const room = LivechatRooms.findOneById(roomId); + + if (!room) { + throw new Error('error-invalid-room'); + } + + if (!canAccessRoom(room, user)) { + throw new Error('error-not-allowed'); + } + + addUserToRoom(roomId, user); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.js b/apps/meteor/app/livechat/server/api/v1/transcript.js deleted file mode 100644 index d57b5277a2a..00000000000 --- a/apps/meteor/app/livechat/server/api/v1/transcript.js +++ /dev/null @@ -1,22 +0,0 @@ -import { check } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; - -API.v1.addRoute('livechat/transcript', { - async post() { - check(this.bodyParams, { - token: String, - rid: String, - email: String, - }); - - const { token, rid, email } = this.bodyParams; - if (!(await Livechat.sendTranscript({ token, rid, email }))) { - return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') }); - } - - return API.v1.success({ message: TAPi18n.__('Livechat_transcript_sent') }); - }, -}); diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.ts b/apps/meteor/app/livechat/server/api/v1/transcript.ts new file mode 100644 index 00000000000..a857c93d5f3 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/transcript.ts @@ -0,0 +1,21 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { isPOSTLivechatTranscriptParams } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute( + 'livechat/transcript', + { validateParams: isPOSTLivechatTranscriptParams }, + { + async post() { + const { token, rid, email } = this.bodyParams; + // @ts-expect-error -- typings on sendtranscript are wrong + if (!(await Livechat.sendTranscript({ token, rid, email }))) { + return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') }); + } + + return API.v1.success({ message: TAPi18n.__('Livechat_transcript_sent') }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/transfer.js b/apps/meteor/app/livechat/server/api/v1/transfer.ts similarity index 80% rename from apps/meteor/app/livechat/server/api/v1/transfer.js rename to apps/meteor/app/livechat/server/api/v1/transfer.ts index 726906330bb..22a70d20533 100644 --- a/apps/meteor/app/livechat/server/api/v1/transfer.js +++ b/apps/meteor/app/livechat/server/api/v1/transfer.ts @@ -1,6 +1,3 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - import { LivechatRooms } from '../../../../models/server'; import { API } from '../../../../api/server'; import { findLivechatTransferHistory } from '../lib/transfer'; @@ -10,15 +7,11 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-rooms'] }, { async get() { - check(this.urlParams, { - rid: String, - }); - const { rid } = this.urlParams; const room = LivechatRooms.findOneById(rid, { _id: 1 }); if (!room) { - throw new Meteor.Error('invalid-room'); + throw new Error('invalid-room'); } const { offset, count } = this.getPaginationItems(); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.ts similarity index 62% rename from apps/meteor/app/livechat/server/api/v1/videoCall.js rename to apps/meteor/app/livechat/server/api/v1/videoCall.ts index 0ea42b293cd..d7b57536d21 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.js +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -1,41 +1,40 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; +import { isGETWebRTCCall, isPUTWebRTCCallId } from '@rocket.chat/rest-typings'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Settings } from '@rocket.chat/models'; -import { Messages, Rooms } from '../../../../models'; +import { Messages, Rooms } from '../../../../models/server'; import { settings as rcSettings } from '../../../../settings/server'; import { API } from '../../../../api/server'; import { settings } from '../lib/livechat'; -import { canSendMessage } from '../../../../authorization'; +import { canSendMessage } from '../../../../authorization/server'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute( 'livechat/webrtc.call', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETWebRTCCall }, { async get() { - check(this.queryParams, { - rid: Match.Maybe(String), - }); - - const room = canSendMessage(this.queryParams.rid, { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }); + const room = canSendMessage( + this.queryParams.rid, + { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }, + {}, + ); if (!room) { - throw new Meteor.Error('invalid-room'); + throw new Error('invalid-room'); } const webrtcCallingAllowed = rcSettings.get('WebRTC_Enabled') === true && rcSettings.get('Omnichannel_call_provider') === 'WebRTC'; if (!webrtcCallingAllowed) { - throw new Meteor.Error('webRTC calling not enabled'); + throw new Error('webRTC calling not enabled'); } const config = await settings(); if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) { - throw new Meteor.Error('invalid-livechat-config'); + throw new Error('invalid-livechat-config'); } let { callStatus } = room; @@ -66,33 +65,28 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/webrtc.call/:callId', - { authRequired: true, permissionsRequired: ['view-l-room'] }, + { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTWebRTCCallId }, { async put() { - check(this.urlParams, { - callId: String, - }); - - check(this.bodyParams, { - rid: Match.Maybe(String), - status: Match.Maybe(String), - }); - const { callId } = this.urlParams; const { rid, status } = this.bodyParams; - const room = canSendMessage(rid, { - uid: this.userId, - username: this.user.username, - type: this.user.type, - }); + const room = canSendMessage( + rid, + { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }, + {}, + ); if (!room) { - throw new Meteor.Error('invalid-room'); + throw new Error('invalid-room'); } const call = await Messages.findOneById(callId); if (!call || call.t !== 'livechat_webrtc_video_call') { - throw new Meteor.Error('invalid-callId'); + throw new Error('invalid-callId'); } Livechat.updateCallStatus(callId, rid, status, this.user); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 12332750a4d..29ef6bf6b76 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -1,6 +1,6 @@ import { Box, Pagination } from '@rocket.chat/fuselage'; import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import type { LivechatRoomsProps } from '@rocket.chat/rest-typings'; +import type { GETLivechatRoomsParams } from '@rocket.chat/rest-typings'; import { usePermission, useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import moment from 'moment'; import React, { memo, ReactElement, useCallback, useMemo, useState } from 'react'; @@ -41,7 +41,7 @@ type useQueryType = ( }, customFields: { [key: string]: string } | undefined, [column, direction]: [string, 'asc' | 'desc'], -) => LivechatRoomsProps | undefined; +) => GETLivechatRoomsParams | undefined; const sortDir = (sortDir: 'asc' | 'desc'): 1 | -1 => (sortDir === 'asc' ? 1 : -1); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx index 7dfad23e685..e40f0f6554b 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx @@ -1,5 +1,5 @@ +import { ILivechatCustomField } from '@rocket.chat/core-typings'; import { Field, TextInput, Select } from '@rocket.chat/fuselage'; -import { OmnichannelCustomFieldEndpointPayload } from '@rocket.chat/rest-typings/src/v1/omnichannel'; import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; import React, { ReactElement, Dispatch, SetStateAction, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -8,7 +8,7 @@ import VerticalBar from '../../../components/VerticalBar'; type CustomFieldsVerticalBarProps = { setCustomFields: Dispatch<SetStateAction<{ [key: string]: string } | undefined>>; - allCustomFields: OmnichannelCustomFieldEndpointPayload[]; + allCustomFields: ILivechatCustomField[]; }; const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFieldsVerticalBarProps): ReactElement => { @@ -34,7 +34,7 @@ const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFie {/* TODO: REMOVE FILTER ONCE THE ENDPOINT SUPPORTS A SCOPE PARAMETER */} {allCustomFields .filter((customField) => customField.scope !== 'visitor') - .map((customField: OmnichannelCustomFieldEndpointPayload) => + .map((customField: ILivechatCustomField) => customField.type === 'select' ? ( <Field> <Field.Label>{customField.label}</Field.Label> @@ -43,7 +43,7 @@ const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFie name={customField._id} control={control} render={({ field }): ReactElement => ( - <Select {...field} options={customField.options.split(',').map((item) => [item, item])} /> + <Select {...field} options={(customField.options || '').split(',').map((item) => [item, item])} /> )} /> </Field.Row> diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index 256d4b625c5..fb983dd79a1 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -121,7 +121,7 @@ function ContactTable({ setContactReload }: ContactTableProps): ReactElement { {result.phase === AsyncStatePhase.RESOLVED && ( <GenericTableBody> {result.value.visitors.map(({ _id, username, fname, name, visitorEmails, phone, lastChat }) => { - const phoneNumber = phone?.length && phone[0].phoneNumber; + const phoneNumber = (phone?.length && phone[0].phoneNumber) || ''; const visitorEmail = visitorEmails?.length && visitorEmails[0].address; return ( diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx index a0c87608166..a0400c2795a 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx @@ -21,6 +21,9 @@ export const ComposerOmnichannelInquiry = (): ReactElement => { if (!result.isSuccess) { return; } + if (!result.data.inquiry) { + return; + } try { await takeInquiry(result.data.inquiry._id); } catch (error) { diff --git a/apps/meteor/ee/app/api-enterprise/server/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts similarity index 74% rename from apps/meteor/ee/app/api-enterprise/server/canned-responses.js rename to apps/meteor/ee/app/api-enterprise/server/canned-responses.ts index bcc719ee0b4..8c246a8ccb1 100644 --- a/apps/meteor/ee/app/api-enterprise/server/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; +import { isPOSTCannedResponsesProps, isDELETECannedResponsesProps, isCannedResponsesProps } from '@rocket.chat/rest-typings'; import { API } from '../../../../app/api/server'; import { findAllCannedResponses, findAllCannedResponsesFilter, findOneCannedResponse } from './lib/canned-responses'; API.v1.addRoute( 'canned-responses.get', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-canned-responses'] }, { async get() { return API.v1.success({ @@ -18,15 +18,16 @@ API.v1.addRoute( API.v1.addRoute( 'canned-responses', - { authRequired: true }, + { + authRequired: true, + permissionsRequired: { GET: ['view-canned-responses'], POST: ['save-canned-responses'], DELETE: ['remove-canned-responses'] }, + validateParams: { POST: isPOSTCannedResponsesProps, DELETE: isDELETECannedResponsesProps, GET: isCannedResponsesProps }, + }, { async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); const { shortcut, text, scope, tags, departmentId, createdBy } = this.requestParams(); - check(shortcut, Match.Maybe(String)); - check(text, Match.Maybe(String)); - check(tags, Match.Maybe([String])); const { cannedResponses, total } = await findAllCannedResponsesFilter({ shortcut, text, @@ -50,14 +51,6 @@ API.v1.addRoute( }); }, async post() { - check(this.bodyParams, { - _id: Match.Maybe(String), - shortcut: String, - text: String, - scope: String, - tags: Match.Maybe([String]), - departmentId: Match.Maybe(String), - }); const { _id, shortcut, text, scope, departmentId, tags } = this.bodyParams; Meteor.runAsUser(this.userId, () => { Meteor.call('saveCannedResponse', _id, { @@ -72,7 +65,6 @@ API.v1.addRoute( }, async delete() { const { _id } = this.requestParams(); - check(_id, String); Meteor.runAsUser(this.userId, () => { Meteor.call('removeCannedResponse', _id); }); @@ -83,17 +75,19 @@ API.v1.addRoute( API.v1.addRoute( 'canned-responses/:_id', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-canned-responses'] }, { async get() { const { _id } = this.urlParams; - check(_id, String); - const cannedResponse = await findOneCannedResponse({ userId: this.userId, _id, }); + if (!cannedResponse) { + return API.v1.notFound(); + } + return API.v1.success({ cannedResponse }); }, }, diff --git a/apps/meteor/ee/app/api-enterprise/server/index.js b/apps/meteor/ee/app/api-enterprise/server/index.ts similarity index 100% rename from apps/meteor/ee/app/api-enterprise/server/index.js rename to apps/meteor/ee/app/api-enterprise/server/index.ts diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js index 1073750293b..9bbeb08187d 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js @@ -5,10 +5,6 @@ import { hasPermissionAsync } from '../../../../../app/authorization/server/func import LivechatUnit from '../../../models/server/models/LivechatUnit'; export async function findAllCannedResponses({ userId }) { - if (!(await hasPermissionAsync(userId, 'view-canned-responses'))) { - throw new Error('error-not-authorized'); - } - // If the user is an admin or livechat manager, get his own responses and all responses from all departments if (await hasPermissionAsync(userId, 'view-all-canned-responses')) { return CannedResponse.find({ @@ -73,10 +69,6 @@ export async function findAllCannedResponses({ userId }) { } export async function findAllCannedResponsesFilter({ userId, shortcut, text, departmentId, scope, createdBy, tags = [], options = {} }) { - if (!(await hasPermissionAsync(userId, 'view-canned-responses'))) { - throw new Error('error-not-authorized'); - } - let extraFilter = []; // if user cannot see all, filter to private + public + departments user is in if (!(await hasPermissionAsync(userId, 'view-all-canned-responses'))) { @@ -168,10 +160,6 @@ export async function findAllCannedResponsesFilter({ userId, shortcut, text, dep } export async function findOneCannedResponse({ userId, _id }) { - if (!(await hasPermissionAsync(userId, 'view-canned-responses'))) { - throw new Error('error-not-authorized'); - } - if (await hasPermissionAsync(userId, 'view-all-canned-responses')) { return CannedResponse.findOneById(_id); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts similarity index 69% rename from apps/meteor/ee/app/livechat-enterprise/server/api/agents.js rename to apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts index a52b1b4dbfa..67c126172a7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts @@ -1,4 +1,8 @@ -import { Match, check } from 'meteor/check'; +import { + isLivechatAnalyticsAgentsTotalServiceTimeProps, + isLivechatAnalyticsAgentsAverageServiceTimeProps, + isLivechatAnalyticsAgentsAvailableForServiceHistoryProps, +} from '@rocket.chat/rest-typings'; import { API } from '../../../../../app/api/server'; import { @@ -9,28 +13,25 @@ import { API.v1.addRoute( 'livechat/analytics/agents/average-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); - - check(start, String); - check(end, String); + const { start, end } = this.requestParams(); if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { agents, total } = findAllAverageServiceTime({ - start, - end, + start: startDate, + end: endDate, options: { offset, count }, }); return API.v1.success({ @@ -45,28 +46,25 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/total-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); - - check(start, String); - check(end, String); + const { start, end } = this.requestParams(); if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { agents, total } = findAllServiceTime({ - start, - end, + start: startDate, + end: endDate, options: { offset, count }, }); return API.v1.success({ @@ -81,30 +79,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/available-for-service-history', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentsAvailableForServiceHistoryProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { fullReport } = this.requestParams(); - check(start, String); - check(end, String); - check(fullReport, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { agents, total } = findAvailableServiceTimeHistory({ - start, - end, + start: startDate, + end: endDate, fullReport: fullReport && fullReport === 'true', options: { offset, count }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts similarity index 67% rename from apps/meteor/ee/app/livechat-enterprise/server/api/departments.js rename to apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts index 7b160fa2996..61ac00143c9 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts @@ -1,4 +1,14 @@ import { Match, check } from 'meteor/check'; +import { + isLivechatAnalyticsDepartmentsAmountOfChatsProps, + isLivechatAnalyticsDepartmentsAverageServiceTimeProps, + isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps, + isLivechatAnalyticsDepartmentsTotalServiceTimeProps, + isLivechatAnalyticsDepartmentsAverageWaitingTimeProps, + isLivechatAnalyticsDepartmentsTotalTransferredChatsProps, + isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps, + isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps, +} from '@rocket.chat/rest-typings'; import { API } from '../../../../../app/api/server'; import { @@ -14,31 +24,26 @@ import { API.v1.addRoute( 'livechat/analytics/departments/amount-of-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { answered, departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(answered, Match.Maybe(String)); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllRooms({ - start, - end, + start: startDate, + end: endDate, answered: answered && answered === 'true', departmentId, options: { offset, count }, @@ -55,30 +60,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsAverageServiceTimeProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllAverageServiceTime({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -94,30 +99,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-chat-duration-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllAverageOfChatDurationTime({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -133,30 +138,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsTotalServiceTimeProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllServiceTime({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -172,11 +177,15 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/average-waiting-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsAverageWaitingTimeProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); check(start, String); @@ -186,16 +195,16 @@ API.v1.addRoute( if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllAverageWaitingTime({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -211,30 +220,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-transferred-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsTotalTransferredChatsProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllNumberOfTransferredRooms({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -250,30 +259,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/total-abandoned-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findAllNumberOfAbandonedRooms({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); @@ -289,30 +298,30 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/departments/percentage-abandoned-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps, + }, { async get() { const { offset, count } = this.getPaginationItems(); - let { start, end } = this.requestParams(); + const { start, end } = this.requestParams(); const { departmentId } = this.requestParams(); - check(start, String); - check(end, String); - check(departmentId, Match.Maybe(String)); - if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - start = new Date(start); + const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - end = new Date(end); + const endDate = new Date(end); const { departments, total } = findPercentageOfAbandonedRooms({ - start, - end, + start: startDate, + end: endDate, departmentId, options: { offset, count }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.ts similarity index 80% rename from apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js rename to apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.ts index a3004579286..1c2fb444212 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/inquiries.ts @@ -2,7 +2,15 @@ import { LivechatInquiry, Users, LivechatPriority } from '@rocket.chat/models'; import { LivechatEnterprise } from '../../lib/LivechatEnterprise'; -export async function setPriorityToInquiry({ userId, roomId, priority }) { +export async function setPriorityToInquiry({ + userId, + roomId, + priority, +}: { + userId: string; + roomId: string; + priority: string; +}): Promise<void> { const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { status: 1 } }); if (!inquiry || inquiry.status !== 'queued') { throw new Error('error-invalid-inquiry'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.ts similarity index 63% rename from apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js rename to apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.ts index 636063f26f1..1010901f937 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/monitors.ts @@ -1,7 +1,21 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Users } from '@rocket.chat/models'; +import type { PaginatedResult } from '@rocket.chat/rest-typings'; +import type { ILivechatMonitor, IUser } from '@rocket.chat/core-typings'; -export async function findMonitors({ text, pagination: { offset, count, sort } }) { +export async function findMonitors({ + text, + pagination: { offset, count, sort }, +}: { + text?: string; + pagination: { + offset: number; + count: number; + sort: { + [key: string]: 1 | -1; + }; + }; +}): Promise<PaginatedResult<{ monitors: ILivechatMonitor[] }>> { const query = {}; if (text) { const filterReg = new RegExp(escapeRegExp(text), 'i'); @@ -34,9 +48,9 @@ export async function findMonitors({ text, pagination: { offset, count, sort } } }; } -export async function findMonitorByUsername({ username }) { +export async function findMonitorByUsername({ username }: { username: string }): Promise<IUser> { const user = await Users.findOne( - { username }, + { username, roles: 'livechat-monitor' }, { projection: { username: 1, @@ -49,7 +63,7 @@ export async function findMonitorByUsername({ username }) { }, ); - if (!user || !(await Users.isUserInRole(user._id, 'livechat-monitor'))) { + if (!user) { throw new Error('invalid-user'); } diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index 58800626b04..cd8f318edf4 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -1,11 +1,11 @@ -import type { IRocketChatRecord } from '@rocket.chat/core-typings'; +import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; import type { ICannedResponseModel } from '@rocket.chat/model-typings'; import type { Db } from 'mongodb'; import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; // TODO need to define type for CannedResponse object -export class CannedResponseRaw extends BaseRaw<IRocketChatRecord> implements ICannedResponseModel { +export class CannedResponseRaw extends BaseRaw<IOmnichannelCannedResponse> implements ICannedResponseModel { constructor(db: Db) { super(db, 'canned_response'); } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 1ad17f14861..1396343f2c6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -284,7 +284,7 @@ "fibers": "^5.0.1", "file-type": "^16.5.3", "filenamify": "^4.3.0", - "filesize": "^3.6.1", + "filesize": "9.0.11", "google-libphonenumber": "^3.2.28", "googleapis": "^104.0.0", "grapheme-splitter": "^1.0.4", diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 538e701c8d1..4c834dd8106 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -227,12 +227,22 @@ describe('LIVECHAT - rooms', function () { }); it('should return an array of rooms when the query params is all valid', (done) => { request - .get( - api(`livechat/rooms?agents[]=teste&departamentId=123&open=true&createdAt={"start": "2018-01-26T00:11:22.345Z", "end": "2018-01-26T00:11:22.345Z"} - &closedAt={"start": "2018-01-26T00:11:22.345Z", "end": "2018-01-26T00:11:22.345Z"}&tags[]=rocket - &customFields={"docId": "031041"}&count=3&offset=1&sort={"_updatedAt": 1}&fields={"msgs": 0}&roomName=test`), - ) + .get(api(`livechat/rooms`)) .set(credentials) + .query({ + 'agents[]': 'teste', + 'departmentId': '123', + 'open': true, + 'createdAt': '{"start":"2018-01-26T00:11:22.345Z","end":"2018-01-26T00:11:22.345Z"}', + 'closedAt': '{"start":"2018-01-26T00:11:22.345Z","end":"2018-01-26T00:11:22.345Z"}', + 'tags[]': 'rocket', + 'customFields': '{ "docId": "031041" }', + 'count': 3, + 'offset': 1, + 'sort': '{ "_updatedAt": 1 }', + 'fields': '{ "msgs": 0 }', + 'roomName': 'test', + }) .expect('Content-Type', 'application/json') .expect(200) .expect((res: Response) => { @@ -246,8 +256,9 @@ describe('LIVECHAT - rooms', function () { }); it('should not cause issues when the customFields is empty', (done) => { request - .get(api(`livechat/rooms?customFields={}&roomName=test`)) + .get(api(`livechat/rooms`)) .set(credentials) + .query({ customFields: {}, roomName: 'test' }) .expect('Content-Type', 'application/json') .expect(200) .expect((res: Response) => { @@ -261,8 +272,9 @@ describe('LIVECHAT - rooms', function () { }); it('should throw an error if customFields param is not a object', (done) => { request - .get(api(`livechat/rooms?customFields=string`)) + .get(api(`livechat/rooms`)) .set(credentials) + .query({ customFields: 'string' }) .expect('Content-Type', 'application/json') .expect(400) .expect((res: Response) => { @@ -323,10 +335,6 @@ describe('LIVECHAT - rooms', function () { }) .expect('Content-Type', 'application/json') .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[invalid-token]'); - }) .end(done); }); @@ -340,10 +348,6 @@ describe('LIVECHAT - rooms', function () { }) .expect('Content-Type', 'application/json') .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[invalid-room]'); - }) .end(done); }); @@ -375,10 +379,6 @@ describe('LIVECHAT - rooms', function () { }) .expect('Content-Type', 'application/json') .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[room-closed]'); - }) .end(done); }); }); @@ -528,7 +528,6 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[invalid-token]'); }) .end(done); }); @@ -546,7 +545,6 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[invalid-room]'); }) .end(done); }); @@ -564,7 +562,6 @@ describe('LIVECHAT - rooms', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('[invalid-data]'); }) .end(done); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index e8ed549771f..7ca1e002d23 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -28,7 +28,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/conversation-totalizers')) + .get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -59,7 +59,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/productivity-totalizers')) + .get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -90,7 +90,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/chats-totalizers')) + .get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -121,7 +121,9 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/agents-productivity-totalizers')) + .get( + api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'), + ) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -153,7 +155,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/charts/chats')) + .get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -182,7 +184,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/charts/chats-per-agent')) + .get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -238,7 +240,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/charts/chats-per-department')) + .get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -264,7 +266,7 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-manager', []).then(() => { request - .get(api('livechat/analytics/dashboards/charts/timings')) + .get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index 88931830315..d8aaa9f7670 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -380,7 +380,13 @@ describe('LIVECHAT - visitors', function () { it('should return an error when the user doesnt have the right permissions', (done) => { updatePermission('view-l-room', []) .then(() => - request.get(api('livechat/visitors.autocomplete')).set(credentials).expect('Content-Type', 'application/json').expect(403), + request + .get(api('livechat/visitors.autocomplete')) + .set(credentials) + .query({ selector: 'invalid' }) + .query({ selector: 'xxx' }) + .expect('Content-Type', 'application/json') + .expect(403), ) .then(() => done()); }); @@ -394,7 +400,7 @@ describe('LIVECHAT - visitors', function () { .expect(400) .expect((res: Response) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal("The 'selector' param is required"); + expect(res.body.error).to.be.equal("must have required property 'selector' [invalid-params]"); }) .end(done); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index eae77c01e93..f6f059e5b42 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -42,6 +42,7 @@ describe('LIVECHAT - Utils', function () { }); it('should fail if setting Livechat_offline_email is not setup or is invalid', async () => { await updateSetting('Livechat_validate_offline_email', false); + await updateSetting('Livechat_validate_offline_email', 'afsdxcvxc'); await request .post(api('livechat/offline.message')) .set(credentials) diff --git a/packages/model-typings/src/models/ICannedResponseModel.ts b/packages/model-typings/src/models/ICannedResponseModel.ts index 409cd959b7d..cc87242f950 100644 --- a/packages/model-typings/src/models/ICannedResponseModel.ts +++ b/packages/model-typings/src/models/ICannedResponseModel.ts @@ -1,8 +1,8 @@ -import type { IRocketChatRecord } from '@rocket.chat/core-typings'; +import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICannedResponseModel extends IBaseModel<IRocketChatRecord> { +export interface ICannedResponseModel extends IBaseModel<IOmnichannelCannedResponse> { // } diff --git a/packages/rest-typings/src/helpers/Deprecated.ts b/packages/rest-typings/src/helpers/Deprecated.ts new file mode 100644 index 00000000000..d3c9514b454 --- /dev/null +++ b/packages/rest-typings/src/helpers/Deprecated.ts @@ -0,0 +1 @@ +export type Deprecated<T> = (T & { warning: string }) | T; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 7b4a176baf8..e940eacd785 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -13,17 +13,26 @@ import type { IRoom, ISetting, ILivechatPriority, + ILivechatAgentActivity, + ILivechatCustomField, + IOmnichannelSystemMessage, + Serialized, + ILivechatBusinessHour, + ILivechatTrigger, ILivechatInquiryRecord, } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; +import type { WithId } from 'mongodb'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; +import type { Deprecated } from '../helpers/Deprecated'; type booleanString = 'true' | 'false'; const ajv = new Ajv({ coerceTypes: true, + allowUnionTypes: true, }); type LivechatVisitorsInfo = { @@ -609,7 +618,7 @@ const LivechatDepartmentsByUnitIdSchema = { export const isLivechatDepartmentsByUnitIdProps = ajv.compile<LivechatDepartmentsByUnitIdProps>(LivechatDepartmentsByUnitIdSchema); -type LivechatUsersManagerGETProps = PaginatedRequest<{ text?: string }>; +type LivechatUsersManagerGETProps = PaginatedRequest<{ text?: string; fields?: string }>; const LivechatUsersManagerGETSchema = { type: 'object', @@ -634,6 +643,10 @@ const LivechatUsersManagerGETSchema = { type: 'string', nullable: true, }, + fields: { + type: 'string', + nullable: true, + }, }, required: [], additionalProperties: false, @@ -712,23 +725,13 @@ const LivechatQueuePropsSchema = { export const isLivechatQueueProps = ajv.compile<LivechatQueueProps>(LivechatQueuePropsSchema); -export type OmnichannelCustomFieldEndpointPayload = { - defaultValue: string; - label: string; - options: string; - public: false; - regexp: string; - required: boolean; - scope: 'visitor' | 'room'; - type: 'select' | 'text'; - visibility: string; - _id: string; -}; - type CannedResponsesProps = PaginatedRequest<{ scope?: string; departmentId?: string; text?: string; + shortcut?: string; + tags?: string[]; + createdBy?: string; }>; const CannedResponsesPropsSchema = { @@ -742,6 +745,21 @@ const CannedResponsesPropsSchema = { type: 'string', nullable: true, }, + shortcut: { + type: 'string', + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + createdBy: { + type: 'string', + nullable: true, + }, text: { type: 'string', nullable: true, @@ -758,10 +776,6 @@ const CannedResponsesPropsSchema = { type: 'string', nullable: true, }, - query: { - type: 'string', - nullable: true, - }, }, additionalProperties: false, }; @@ -775,6 +789,7 @@ const LivechatCustomFieldsSchema = { properties: { text: { type: 'string', + nullable: true, }, count: { type: 'number', @@ -793,7 +808,6 @@ const LivechatCustomFieldsSchema = { nullable: true, }, }, - required: ['text'], additionalProperties: false, }; @@ -862,7 +876,7 @@ const LivechatRoomsSchema = { export const isLivechatRoomsProps = ajv.compile<LivechatRoomsProps>(LivechatRoomsSchema); -type LivechatRidMessagesProps = PaginatedRequest<{ query: string }>; +type LivechatRidMessagesProps = PaginatedRequest; const LivechatRidMessagesSchema = { type: 'object', @@ -879,11 +893,8 @@ const LivechatRidMessagesSchema = { type: 'string', nullable: true, }, - query: { - type: 'string', - }, }, - required: ['query'], + required: [], additionalProperties: false, }; @@ -1027,6 +1038,29 @@ export const isGETOmnichannelContactProps = ajv.compile<GETOmnichannelContactPro type GETOmnichannelContactSearchProps = { email: string } | { phone: string }; +type LivechatAnalyticsAgentsAverageServiceTimeProps = { + start: string; + end: string; +}; + +const LivechatAnalyticsAgentsAverageServiceTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsAgentsAverageServiceTimeProps = ajv.compile<LivechatAnalyticsAgentsAverageServiceTimeProps>( + LivechatAnalyticsAgentsAverageServiceTimeSchema, +); + const GETOmnichannelContactSearchSchema = { anyOf: [ { @@ -1054,197 +1088,1693 @@ const GETOmnichannelContactSearchSchema = { export const isGETOmnichannelContactSearchProps = ajv.compile<GETOmnichannelContactSearchProps>(GETOmnichannelContactSearchSchema); -export type OmnichannelEndpoints = { - '/v1/livechat/inquiries.getOne': { - GET: (params: { roomId: string }) => { - inquiry: ILivechatInquiryRecord; - }; - }; - '/v1/livechat/appearance': { - GET: () => { - appearance: ISetting[]; - }; - }; - '/v1/livechat/visitors.info': { - GET: (params: LivechatVisitorsInfo) => { - visitor: { - visitorEmails: Array<{ - address: string; - }>; - }; - }; - }; - '/v1/livechat/room': { - GET: (params: { token: string; rid: IRoom['_id'] }) => { - room: IOmnichannelRoom; - }; - }; - '/v1/livechat/room.onHold': { - POST: (params: LivechatRoomOnHold) => void; - }; - '/v1/livechat/room.join': { - GET: (params: LiveChatRoomJoin) => { success: boolean }; - }; - '/v1/livechat/room.forward': { - POST: (params: LiveChatRoomForward) => { success: boolean }; - }; - '/v1/livechat/monitors': { - GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ - monitors: ILivechatMonitor[]; - }>; - }; - '/v1/livechat/monitors/:username': { - GET: () => ILivechatMonitor; - }; - '/v1/livechat/tags': { - GET: (params: LivechatTagsListProps) => PaginatedResult<{ - tags: ILivechatTag[]; - }>; - }; - '/v1/livechat/tags/:tagId': { - GET: () => ILivechatTag | null; - }; - '/v1/livechat/department': { - GET: (params: LivechatDepartmentProps) => PaginatedResult<{ - departments: ILivechatDepartment[]; - }>; - POST: (params: { department: Partial<ILivechatDepartment>; agents: string[] }) => { - department: ILivechatDepartment; - agents: any[]; - }; - }; - '/v1/livechat/department/:_id': { - GET: (params: LivechatDepartmentId) => { - department: ILivechatDepartmentRecord | null; - agents?: ILivechatDepartmentAgents[]; - }; - PUT: (params: { department: Partial<ILivechatDepartment>[]; agents: any[] }) => { - department: ILivechatDepartment; - agents: ILivechatDepartmentAgents[]; - }; - DELETE: () => void; - }; - '/v1/livechat/department.autocomplete': { - GET: (params: LivechatDepartmentAutocomplete) => { - items: ILivechatDepartment[]; - }; - }; - '/v1/livechat/department/:departmentId/agents': { - GET: (params: LivechatDepartmentDepartmentIdAgentsGET) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; - POST: (params: LivechatDepartmentDepartmentIdAgentsPOST) => void; - }; - '/v1/livechat/units/:unitId/departments/available': { - GET: (params: LivechatDepartmentsAvailableByUnitIdProps) => PaginatedResult<{ - departments: ILivechatDepartment[]; - }>; - }; - '/v1/livechat/departments.by-unit/': { - GET: (params: LivechatDepartmentsByUnitProps) => PaginatedResult<{ - departments: ILivechatDepartment[]; - }>; - }; +type LivechatAnalyticsAgentsTotalServiceTimeProps = { + start: string; + end: string; +}; - '/v1/livechat/units/:unitId/departments': { - GET: (params: LivechatDepartmentsByUnitIdProps) => PaginatedResult<{ - departments: ILivechatDepartment[]; - }>; - }; +const LivechatAnalyticsAgentsTotalServiceTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; - '/v1/livechat/department.listByIds': { - GET: (params: { ids: string[]; fields?: Record<string, unknown> }) => { - departments: ILivechatDepartment[]; - }; - }; +export const isLivechatAnalyticsAgentsTotalServiceTimeProps = ajv.compile<LivechatAnalyticsAgentsTotalServiceTimeProps>( + LivechatAnalyticsAgentsTotalServiceTimeSchema, +); - '/v1/livechat/custom-fields': { - GET: (params?: LivechatCustomFieldsProps) => PaginatedResult<{ - customFields: [OmnichannelCustomFieldEndpointPayload]; - }>; - }; - '/v1/livechat/rooms': { - GET: (params: LivechatRoomsProps) => PaginatedResult<{ - rooms: IOmnichannelRoom[]; - }>; - }; - '/v1/livechat/:rid/messages': { - GET: (params: LivechatRidMessagesProps) => PaginatedResult<{ - messages: IMessage[]; - }>; - }; +type LivechatAnalyticsAgentsAvailableForServiceHistoryProps = { + start: string; + end: string; + fullReport?: string; +}; - '/v1/livechat/users/manager': { - GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ - users: ILivechatAgent[]; - }>; - POST: (params: { username: string }) => { success: boolean }; - }; +const LivechatAnalyticsAgentsAvailableForServiceHistorySchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + fullReport: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; - '/v1/livechat/users/manager/:_id': { - GET: ( - params: PaginatedRequest<{ - text: string; - }>, - ) => { user: ILivechatAgent }; - DELETE: () => void; - }; +export const isLivechatAnalyticsAgentsAvailableForServiceHistoryProps = ajv.compile<LivechatAnalyticsAgentsAvailableForServiceHistoryProps>( + LivechatAnalyticsAgentsAvailableForServiceHistorySchema, +); - '/v1/livechat/users/agent': { - GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ - users: ILivechatAgent[]; - }>; - POST: (params: LivechatUsersManagerPOSTProps) => { success: boolean }; - }; +type LivechatAnalyticsDepartmentsAmountOfChatsProps = { + start: string; + end: string; + answered?: string; + departmentId?: string; +}; - '/v1/livechat/users/agent/:_id': { - GET: ( - params: PaginatedRequest<{ - text: string; - }>, - ) => { user: Pick<ILivechatAgent, '_id' | 'username' | 'name' | 'status' | 'statusLivechat' | 'emails' | 'livechat'> }; - DELETE: () => { success: boolean }; - }; +const LivechatAnalyticsDepartmentsAmountOfChatsSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + answered: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; - '/v1/livechat/visitor': { - POST: (params: { visitor: ILivechatVisitorDTO }) => { - visitor: ILivechatVisitor; - }; - }; +export const isLivechatAnalyticsDepartmentsAmountOfChatsProps = ajv.compile<LivechatAnalyticsDepartmentsAmountOfChatsProps>( + LivechatAnalyticsDepartmentsAmountOfChatsSchema, +); - '/v1/livechat/visitor/:token': { - GET: (params?: LivechatVisitorTokenGet) => { visitor: ILivechatVisitor }; - DELETE: (params: LivechatVisitorTokenDelete) => { - visitor: { _id: string; ts: string }; - }; - }; +type LivechatAnalyticsDepartmentsAverageServiceTimeProps = { + start: string; + end: string; + departmentId?: string; +}; - '/v1/livechat/visitor/:token/room': { - GET: (params: LivechatVisitorTokenRoom) => { rooms: IOmnichannelRoom[] }; - }; +const LivechatAnalyticsDepartmentsAverageServiceTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; - '/v1/livechat/visitor.callStatus': { - POST: (params: LivechatVisitorCallStatus) => { - token: string; - callStatus: string; - }; - }; +export const isLivechatAnalyticsDepartmentsAverageServiceTimeProps = ajv.compile<LivechatAnalyticsDepartmentsAverageServiceTimeProps>( + LivechatAnalyticsDepartmentsAverageServiceTimeSchema, +); - '/v1/livechat/visitor.status': { - POST: (params: LivechatVisitorStatus) => { - token: string; - status: string; - }; +type LivechatAnalyticsDepartmentsAverageChatDurationTimeProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsAverageChatDurationTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps = + ajv.compile<LivechatAnalyticsDepartmentsAverageChatDurationTimeProps>(LivechatAnalyticsDepartmentsAverageChatDurationTimeSchema); + +type LivechatAnalyticsDepartmentsTotalServiceTimeProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsTotalServiceTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsTotalServiceTimeProps = ajv.compile<LivechatAnalyticsDepartmentsTotalServiceTimeProps>( + LivechatAnalyticsDepartmentsTotalServiceTimeSchema, +); + +type LivechatAnalyticsDepartmentsAverageWaitingTimeProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsAverageWaitingTimeSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsAverageWaitingTimeProps = ajv.compile<LivechatAnalyticsDepartmentsAverageWaitingTimeProps>( + LivechatAnalyticsDepartmentsAverageWaitingTimeSchema, +); + +type LivechatAnalyticsDepartmentsTotalTransferredChatsProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsTotalTransferredChatsSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsTotalTransferredChatsProps = ajv.compile<LivechatAnalyticsDepartmentsTotalTransferredChatsProps>( + LivechatAnalyticsDepartmentsTotalTransferredChatsSchema, +); + +type LivechatAnalyticsDepartmentsTotalAbandonedChatsProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsTotalAbandonedChatsSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps = ajv.compile<LivechatAnalyticsDepartmentsTotalAbandonedChatsProps>( + LivechatAnalyticsDepartmentsTotalAbandonedChatsSchema, +); + +type LivechatAnalyticsDepartmentsPercentageAbandonedChatsProps = { + start: string; + end: string; + departmentId?: string; +}; + +const LivechatAnalyticsDepartmentsPercentageAbandonedChatsSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['start', 'end'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps = + ajv.compile<LivechatAnalyticsDepartmentsPercentageAbandonedChatsProps>(LivechatAnalyticsDepartmentsPercentageAbandonedChatsSchema); + +type GETAgentNextToken = { + department?: string; +}; + +const GETAgentNextTokenSchema = { + type: 'object', + properties: { + department: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETAgentNextToken = ajv.compile<GETAgentNextToken>(GETAgentNextTokenSchema); + +type GETLivechatConfigParams = { + token?: string; + department?: string; + businessUnit?: string; +}; + +const GETLivechatConfigParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + nullable: true, + }, + department: { + type: 'string', + nullable: true, + }, + businessUnit: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatConfigParams = ajv.compile<GETLivechatConfigParams>(GETLivechatConfigParamsSchema); + +type POSTLivechatCustomFieldParams = { + token: string; + key: string; + value: string; + overwrite: boolean; +}; + +const POSTLivechatCustomFieldParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + key: { + type: 'string', + }, + value: { + type: 'string', + }, + overwrite: { + type: 'boolean', + }, + }, + required: ['token', 'key', 'value', 'overwrite'], + additionalProperties: false, +}; + +export const isPOSTLivechatCustomFieldParams = ajv.compile<POSTLivechatCustomFieldParams>(POSTLivechatCustomFieldParamsSchema); + +type POSTLivechatCustomFieldsParams = { + token: string; + customFields: { + key: string; + value: string; + overwrite: boolean; + }[]; +}; + +const POSTLivechatCustomFieldsParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + customFields: { + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string', + }, + value: { + type: 'string', + }, + overwrite: { + type: 'boolean', + }, + }, + required: ['key', 'value', 'overwrite'], + additionalProperties: false, + }, + }, + }, + required: ['token', 'customFields'], + additionalProperties: false, +}; + +export const isPOSTLivechatCustomFieldsParams = ajv.compile<POSTLivechatCustomFieldsParams>(POSTLivechatCustomFieldsParamsSchema); + +type GETWebRTCCall = { rid: string }; + +const GETWebRTCCallSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isGETWebRTCCall = ajv.compile<GETWebRTCCall>(GETWebRTCCallSchema); + +type PUTWebRTCCallId = { rid: string; status: string }; + +const PUTWebRTCCallIdSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + status: { + type: 'string', + }, + }, + required: ['rid', 'status'], + additionalProperties: false, +}; + +export const isPUTWebRTCCallId = ajv.compile<PUTWebRTCCallId>(PUTWebRTCCallIdSchema); + +type POSTLivechatTranscriptParams = { + rid: string; + token: string; + email: string; +}; + +const POSTLivechatTranscriptParamsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + token: { + type: 'string', + }, + email: { + type: 'string', + }, + }, + required: ['rid', 'token', 'email'], + additionalProperties: false, +}; + +export const isPOSTLivechatTranscriptParams = ajv.compile<POSTLivechatTranscriptParams>(POSTLivechatTranscriptParamsSchema); + +type POSTLivechatOfflineMessageParams = { + name: string; + email: string; + message: string; + department?: string; + host?: string; +}; + +const POSTLivechatOfflineMessageParamsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + email: { + type: 'string', + }, + message: { + type: 'string', + }, + department: { + type: 'string', + nullable: true, + }, + host: { + type: 'string', + nullable: true, + }, + }, + required: ['name', 'email', 'message'], + additionalProperties: false, +}; + +export const isPOSTLivechatOfflineMessageParams = ajv.compile<POSTLivechatOfflineMessageParams>(POSTLivechatOfflineMessageParamsSchema); + +type POSTLivechatPageVisitedParams = { + token: string; + rid?: string; + pageInfo: { + title: string; + change: string; + location: { + href: string; + }; + }; +}; + +const POSTLivechatPageVisitedParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + nullable: true, + }, + pageInfo: { + type: 'object', + properties: { + title: { + type: 'string', + }, + change: { + type: 'string', + }, + location: { + type: 'object', + properties: { + href: { + type: 'string', + }, + }, + required: ['href'], + additionalProperties: false, + }, + }, + required: ['title', 'change', 'location'], + additionalProperties: false, + }, + }, + required: ['token', 'pageInfo'], + additionalProperties: false, +}; + +export const isPOSTLivechatPageVisitedParams = ajv.compile<POSTLivechatPageVisitedParams>(POSTLivechatPageVisitedParamsSchema); + +type POSTLivechatMessageParams = { + token: string; + rid: string; + msg: string; + _id?: string; + agent?: { + agentId: string; + username: string; + }; +}; + +const POSTLivechatMessageParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + msg: { + type: 'string', + }, + _id: { + type: 'string', + nullable: true, + }, + agent: { + type: 'object', + properties: { + agentId: { + type: 'string', + }, + username: { + type: 'string', + }, + }, + required: ['agentId', 'username'], + additionalProperties: false, + }, + }, + required: ['token', 'rid', 'msg'], + additionalProperties: false, +}; + +export const isPOSTLivechatMessageParams = ajv.compile<POSTLivechatMessageParams>(POSTLivechatMessageParamsSchema); + +type GETLivechatMessageIdParams = { + token: string; + rid: string; +}; + +const GETLivechatMessageIdParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + }, + required: ['token', 'rid'], + additionalProperties: false, +}; + +export const isGETLivechatMessageIdParams = ajv.compile<GETLivechatMessageIdParams>(GETLivechatMessageIdParamsSchema); + +type PUTLivechatMessageIdParams = { + token: string; + rid: string; + msg: string; +}; + +const PUTLivechatMessageIdParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + msg: { + type: 'string', + }, + }, + required: ['token', 'rid', 'msg'], + additionalProperties: false, +}; + +export const isPUTLivechatMessageIdParams = ajv.compile<PUTLivechatMessageIdParams>(PUTLivechatMessageIdParamsSchema); + +type DELETELivechatMessageIdParams = { + token: string; + rid: string; +}; + +const DELETELivechatMessageIdParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['token', 'rid'], + additionalProperties: false, +}; + +export const isDELETELivechatMessageIdParams = ajv.compile<DELETELivechatMessageIdParams>(DELETELivechatMessageIdParamsSchema); + +type GETLivechatMessagesHistoryRidParams = PaginatedRequest<{ + searchText?: string; + token: string; + ls?: string; + end?: string; + limit?: number; +}>; + +const GETLivechatMessagesHistoryRidParamsSchema = { + type: 'object', + properties: { + searchText: { + type: 'string', + nullable: true, + }, + token: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + ls: { + type: 'string', + nullable: true, + }, + end: { + type: 'string', + nullable: true, + }, + limit: { + type: 'number', + nullable: true, + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isGETLivechatMessagesHistoryRidParams = ajv.compile<GETLivechatMessagesHistoryRidParams>( + GETLivechatMessagesHistoryRidParamsSchema, +); + +type GETLivechatMessagesParams = { + visitor: { + token: string; + }; + // Must be of at least 1 item + messages: { + msg: string; + }[]; +}; + +const GETLivechatMessagesParamsSchema = { + type: 'object', + properties: { + visitor: { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + required: ['token'], + additionalProperties: false, + }, + messages: { + type: 'array', + items: { + type: 'object', + properties: { + msg: { + type: 'string', + }, + }, + required: ['msg'], + additionalProperties: false, + }, + minItems: 1, + }, + }, + required: ['visitor', 'messages'], + additionalProperties: false, +}; + +export const isGETLivechatMessagesParams = ajv.compile<GETLivechatMessagesParams>(GETLivechatMessagesParamsSchema); + +type GETLivechatRoomParams = { + token: string; + rid?: string; + agentId?: string; +}; + +const GETLivechatRoomParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + nullable: true, + }, + agentId: { + type: 'string', + nullable: true, + }, + }, + required: ['token'], + additionalProperties: true, +}; + +export const isGETLivechatRoomParams = ajv.compile<GETLivechatRoomParams>(GETLivechatRoomParamsSchema); + +type POSTLivechatRoomCloseParams = { + token: string; + rid: string; +}; + +const POSTLivechatRoomCloseParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + }, + required: ['token', 'rid'], + additionalProperties: false, +}; + +export const isPOSTLivechatRoomCloseParams = ajv.compile<POSTLivechatRoomCloseParams>(POSTLivechatRoomCloseParamsSchema); + +type POSTLivechatRoomTransferParams = { + token: string; + rid: string; + department: string; +}; + +const POSTLivechatRoomTransferParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + department: { + type: 'string', + }, + }, + required: ['token', 'rid', 'department'], + additionalProperties: false, +}; + +export const isPOSTLivechatRoomTransferParams = ajv.compile<POSTLivechatRoomTransferParams>(POSTLivechatRoomTransferParamsSchema); + +type POSTLivechatRoomSurveyParams = { + token: string; + rid: string; + data: { + name: string; + value: string; + }[]; +}; + +const POSTLivechatRoomSurveyParamsSchema = { + type: 'object', + properties: { + token: { + type: 'string', + }, + rid: { + type: 'string', + }, + data: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + value: { + type: 'string', + }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + minItems: 1, + }, + }, + required: ['token', 'rid', 'data'], + additionalProperties: false, +}; + +export const isPOSTLivechatRoomSurveyParams = ajv.compile<POSTLivechatRoomSurveyParams>(POSTLivechatRoomSurveyParamsSchema); + +type PUTLivechatRoomVisitorParams = { + rid: string; + oldVisitorId: string; + newVisitorId: string; +}; + +const PUTLivechatRoomVisitorParamsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + oldVisitorId: { + type: 'string', + }, + newVisitorId: { + type: 'string', + }, + }, + required: ['rid', 'oldVisitorId', 'newVisitorId'], + additionalProperties: false, +}; + +export const isPUTLivechatRoomVisitorParams = ajv.compile<PUTLivechatRoomVisitorParams>(PUTLivechatRoomVisitorParamsSchema); + +type POSTCannedResponsesProps = { + _id?: string; + shortcut: string; + text: string; + scope: string; + departmentId?: string; + tags?: string[]; +}; + +const POSTCannedResponsesPropsSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + shortcut: { + type: 'string', + }, + text: { + type: 'string', + }, + scope: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['shortcut', 'text', 'scope'], + additionalProperties: false, +}; + +export const isPOSTCannedResponsesProps = ajv.compile<POSTCannedResponsesProps>(POSTCannedResponsesPropsSchema); + +type DELETECannedResponsesProps = { + _id: string; +}; + +const DELETECannedResponsesPropsSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + }, + }, + required: ['_id'], + additionalProperties: false, +}; + +export const isDELETECannedResponsesProps = ajv.compile<DELETECannedResponsesProps>(DELETECannedResponsesPropsSchema); + +type POSTLivechatUsersTypeProps = { + username: string; +}; + +const POSTLivechatUsersTypePropsSchema = { + type: 'object', + properties: { + username: { + type: 'string', + }, + }, + required: ['username'], + additionalProperties: false, +}; + +export const isPOSTLivechatUsersTypeProps = ajv.compile<POSTLivechatUsersTypeProps>(POSTLivechatUsersTypePropsSchema); + +type GETLivechatVisitorsPagesVisitedRoomIdParams = PaginatedRequest; + +const GETLivechatVisitorsPagesVisitedRoomIdParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatVisitorsPagesVisitedRoomIdParams = ajv.compile<GETLivechatVisitorsPagesVisitedRoomIdParams>( + GETLivechatVisitorsPagesVisitedRoomIdParamsSchema, +); + +type GETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams = PaginatedRequest; + +const GETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams = + ajv.compile<GETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams>( + GETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParamsSchema, + ); + +type GETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams = PaginatedRequest<{ + searchText?: string; + closedChatsOnly?: string; + servedChatsOnly?: string; +}>; + +const GETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + searchText: { + type: 'string', + nullable: true, + }, + closedChatsOnly: { + type: 'string', + nullable: true, + }, + servedChatsOnly: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams = + ajv.compile<GETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams>( + GETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParamsSchema, + ); + +type GETLivechatVisitorsAutocompleteParams = { selector: string }; + +const GETLivechatVisitorsAutocompleteParamsSchema = { + type: 'object', + properties: { + selector: { + type: 'string', + }, + }, + required: ['selector'], + additionalProperties: false, +}; + +export const isGETLivechatVisitorsAutocompleteParams = ajv.compile<GETLivechatVisitorsAutocompleteParams>( + GETLivechatVisitorsAutocompleteParamsSchema, +); + +type GETLivechatVisitorsSearch = PaginatedRequest<{ term?: string }>; + +const GETLivechatVisitorsSearchSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + term: { + type: 'string', + nullable: true, + }, + }, + required: ['term'], + additionalProperties: false, +}; + +export const isGETLivechatVisitorsSearch = ajv.compile<GETLivechatVisitorsSearch>(GETLivechatVisitorsSearchSchema); + +type GETLivechatAgentsAgentIdDepartmentsParams = { enabledDepartmentsOnly?: booleanString }; + +const GETLivechatAgentsAgentIdDepartmentsParamsSchema = { + type: 'object', + properties: { + enabledDepartmentsOnly: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatAgentsAgentIdDepartmentsParams = ajv.compile<GETLivechatAgentsAgentIdDepartmentsParams>( + GETLivechatAgentsAgentIdDepartmentsParamsSchema, +); + +type GETBusinessHourParams = { _id?: string; type?: string }; + +const GETBusinessHourParamsSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETBusinessHourParams = ajv.compile<GETBusinessHourParams>(GETBusinessHourParamsSchema); + +type GETLivechatTriggersParams = PaginatedRequest; + +const GETLivechatTriggersParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatTriggersParams = ajv.compile<GETLivechatTriggersParams>(GETLivechatTriggersParamsSchema); + +export type GETLivechatRoomsParams = PaginatedRequest<{ + fields?: string; + createdAt?: string; + customFields?: string; + closedAt?: string; + agents?: string[]; + roomName?: string; + departmentId?: string; + open?: string | boolean; + onhold?: string | boolean; + tags?: string[]; +}>; + +const GETLivechatRoomsParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + fields: { + type: 'string', + nullable: true, + }, + createdAt: { + type: 'string', + nullable: true, + }, + customFields: { + type: 'string', + nullable: true, + }, + closedAt: { + type: 'string', + nullable: true, + }, + agents: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + roomName: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + open: { + type: ['string', 'boolean'], + nullable: true, + }, + onhold: { + type: ['string', 'boolean'], + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatRoomsParams = ajv.compile<GETLivechatRoomsParams>(GETLivechatRoomsParamsSchema); + +type GETLivechatQueueParams = PaginatedRequest<{ agentId?: string; departmentId?: string; includeOfflineAgents?: string }>; + +const GETLivechatQueueParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + agentId: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + includeOfflineAgents: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatQueueParams = ajv.compile<GETLivechatQueueParams>(GETLivechatQueueParamsSchema); + +type POSTLivechatFacebookParams = { + text?: string; + attachments?: unknown[]; + mid: string; + page: string; + token: string; + first_name: string; + last_name: string; +}; + +const POSTLivechatFacebookParamsSchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + attachments: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + mid: { + type: 'string', + }, + page: { + type: 'string', + }, + token: { + type: 'string', + }, + first_name: { + type: 'string', + }, + last_name: { + type: 'string', + }, + }, + // Facebook may send additional props + additionalProperties: true, + required: ['mid', 'page', 'token', 'first_name', 'last_name'], +}; + +export const isPOSTLivechatFacebookParams = ajv.compile<POSTLivechatFacebookParams>(POSTLivechatFacebookParamsSchema); + +type GETLivechatInquiriesListParams = PaginatedRequest<{ department?: string }>; + +const GETLivechatInquiriesListParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + department: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatInquiriesListParams = ajv.compile<GETLivechatInquiriesListParams>(GETLivechatInquiriesListParamsSchema); + +type POSTLivechatInquiriesTakeParams = { + inquiryId: string; + userId?: string; +}; + +const POSTLivechatInquiriesTakeParamsSchema = { + type: 'object', + properties: { + inquiryId: { + type: 'string', + }, + userId: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, + required: ['inquiryId'], +}; + +export const isPOSTLivechatInquiriesTakeParams = ajv.compile<POSTLivechatInquiriesTakeParams>(POSTLivechatInquiriesTakeParamsSchema); + +type GETLivechatInquiriesQueuedParams = PaginatedRequest<{ department?: string }>; + +const GETLivechatInquiriesQueuedParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + department: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatInquiriesQueuedParams = ajv.compile<GETLivechatInquiriesQueuedParams>(GETLivechatInquiriesQueuedParamsSchema); + +type GETLivechatInquiriesQueuedForUserParams = PaginatedRequest<{ department?: string }>; + +const GETLivechatInquiriesQueuedForUserParamsSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + department: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, +}; + +export const isGETLivechatInquiriesQueuedForUserParams = ajv.compile<GETLivechatInquiriesQueuedForUserParams>( + GETLivechatInquiriesQueuedForUserParamsSchema, +); + +type GETLivechatInquiriesGetOneParams = { + roomId: string; +}; + +const GETLivechatInquiriesGetOneParamsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['roomId'], +}; + +export const isGETLivechatInquiriesGetOneParams = ajv.compile<GETLivechatInquiriesGetOneParams>(GETLivechatInquiriesGetOneParamsSchema); + +type GETDashboardTotalizers = { + start: string; + end: string; + departmentId?: string; +}; + +const GETLivechatAnalyticsDashboardsConversationTotalizersParamsSchema = { + type: 'object', + properties: { + start: { + type: 'string', + }, + end: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + additionalProperties: false, + required: ['start', 'end'], +}; + +export const isGETDashboardTotalizerParams = ajv.compile<GETDashboardTotalizers>( + GETLivechatAnalyticsDashboardsConversationTotalizersParamsSchema, +); + +type GETDashboardsAgentStatusParams = { + departmentId?: string; +}; + +const GETLivechatAnalyticsDashboardsAgentStatusParamsSchema = { + type: 'object', + properties: { + departmentId: { + type: 'string', + nullable: true, + }, + }, + // FE is sending start/end params, since they use the same container for doing all calls. + // This will prevent FE breaking, but's a TODO for an upcoming engday + additionalProperties: true, +}; + +export const isGETDashboardsAgentStatusParams = ajv.compile<GETDashboardsAgentStatusParams>( + GETLivechatAnalyticsDashboardsAgentStatusParamsSchema, +); + +export type OmnichannelEndpoints = { + '/v1/livechat/appearance': { + GET: () => { + appearance: ISetting[]; + }; + }; + '/v1/livechat/visitors.info': { + GET: (params: LivechatVisitorsInfo) => { + visitor: ILivechatVisitor; + }; + }; + '/v1/livechat/room.onHold': { + POST: (params: LivechatRoomOnHold) => void; + }; + '/v1/livechat/room.join': { + GET: (params: LiveChatRoomJoin) => void; + }; + '/v1/livechat/room.forward': { + POST: (params: LiveChatRoomForward) => void; + }; + '/v1/livechat/monitors': { + GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ + monitors: ILivechatMonitor[]; + }>; + }; + '/v1/livechat/monitors/:username': { + GET: () => ILivechatMonitor; + }; + '/v1/livechat/tags': { + GET: (params: LivechatTagsListProps) => PaginatedResult<{ + tags: ILivechatTag[]; + }>; + }; + '/v1/livechat/tags/:tagId': { + GET: () => ILivechatTag | null; + }; + '/v1/livechat/department': { + GET: (params: LivechatDepartmentProps) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + POST: (params: { department: Partial<ILivechatDepartment>; agents: string[] }) => { + department: ILivechatDepartment; + agents: any[]; + }; + }; + '/v1/livechat/department/:_id': { + GET: (params: LivechatDepartmentId) => { + department: ILivechatDepartmentRecord | null; + agents?: ILivechatDepartmentAgents[]; + }; + PUT: (params: { department: Partial<ILivechatDepartment>[]; agents: any[] }) => { + department: ILivechatDepartment; + agents: ILivechatDepartmentAgents[]; + }; + DELETE: () => void; + }; + '/v1/livechat/department.autocomplete': { + GET: (params: LivechatDepartmentAutocomplete) => { + items: ILivechatDepartment[]; + }; + }; + '/v1/livechat/department/:departmentId/agents': { + GET: (params: LivechatDepartmentDepartmentIdAgentsGET) => PaginatedResult<{ agents: ILivechatDepartmentAgents[] }>; + POST: (params: LivechatDepartmentDepartmentIdAgentsPOST) => void; + }; + '/v1/livechat/units/:unitId/departments/available': { + GET: (params: LivechatDepartmentsAvailableByUnitIdProps) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + '/v1/livechat/departments.by-unit/': { + GET: (params: LivechatDepartmentsByUnitProps) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; }; - '/v1/livechat/queue': { - GET: (params: LivechatQueueProps) => { - queue: { - chats: number; - department: { _id: string; name: string }; - user: { _id: string; username: string; status: string }; - }[]; - count: number; - offset: number; - total: number; + '/v1/livechat/units/:unitId/departments': { + GET: (params: LivechatDepartmentsByUnitIdProps) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + + '/v1/livechat/department.listByIds': { + GET: (params: { ids: string[]; fields?: Record<string, unknown> }) => { + departments: ILivechatDepartment[]; + }; + }; + + '/v1/livechat/custom-fields': { + GET: (params?: LivechatCustomFieldsProps) => PaginatedResult<{ + customFields: ILivechatCustomField[]; + }>; + }; + '/v1/livechat/custom-fields/:_id': { + GET: () => { customField: ILivechatCustomField | null }; + }; + '/v1/livechat/:rid/messages': { + GET: (params: LivechatRidMessagesProps) => PaginatedResult<{ + messages: IMessage[]; + }>; + }; + + '/v1/livechat/users/:type': { + GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: POSTLivechatUsersTypeProps) => { success: boolean }; + }; + + '/v1/livechat/users/:type/:_id': { + GET: () => { user: Pick<ILivechatAgent, '_id' | 'username' | 'name' | 'status' | 'statusLivechat' | 'emails' | 'livechat'> | null }; + DELETE: () => void; + }; + + // For some reason, when using useEndpointData with POST, it's not able to detect the actual type of a path with path params + // So, we need to define the type of the path params here + '/v1/livechat/users/manager': { + GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: POSTLivechatUsersTypeProps) => { success: boolean }; + }; + + '/v1/livechat/users/user': { + GET: (params: LivechatUsersManagerGETProps) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: POSTLivechatUsersTypeProps) => { success: boolean }; + }; + '/v1/livechat/users/manager/:_id': { + GET: () => { user: Pick<ILivechatAgent, '_id' | 'username' | 'name' | 'status' | 'statusLivechat' | 'emails' | 'livechat'> | null }; + DELETE: () => void; + }; + '/v1/livechat/users/user/:_id': { + GET: () => { user: Pick<ILivechatAgent, '_id' | 'username' | 'name' | 'status' | 'statusLivechat' | 'emails' | 'livechat'> | null }; + DELETE: () => void; + }; + + '/v1/livechat/users/agent': { + GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ + users: ILivechatAgent[]; + }>; + POST: (params: LivechatUsersManagerPOSTProps) => { success: boolean }; + }; + + '/v1/livechat/users/agent/:_id': { + GET: ( + params: PaginatedRequest<{ + text: string; + }>, + ) => { user: Pick<ILivechatAgent, '_id' | 'username' | 'name' | 'status' | 'statusLivechat' | 'emails' | 'livechat'> }; + DELETE: () => { success: boolean }; + }; + + '/v1/livechat/visitor': { + POST: (params: { visitor: ILivechatVisitorDTO }) => { + visitor: ILivechatVisitor; + }; + }; + + '/v1/livechat/visitor/:token': { + GET: (params?: LivechatVisitorTokenGet) => { visitor: ILivechatVisitor }; + DELETE: (params: LivechatVisitorTokenDelete) => { + visitor: { _id: string; ts: string }; + }; + }; + + '/v1/livechat/visitor/:token/room': { + GET: (params: LivechatVisitorTokenRoom) => { rooms: IOmnichannelRoom[] }; + }; + + '/v1/livechat/visitor.callStatus': { + POST: (params: LivechatVisitorCallStatus) => { + token: string; + callStatus: string; + }; + }; + + '/v1/livechat/visitor.status': { + POST: (params: LivechatVisitorStatus) => { + token: string; + status: string; }; }; '/v1/livechat/agents/:uid/departments': { @@ -1255,14 +2785,24 @@ export type OmnichannelEndpoints = { GET: (params: CannedResponsesProps) => PaginatedResult<{ cannedResponses: IOmnichannelCannedResponse[]; }>; + POST: (params: POSTCannedResponsesProps) => void; + DELETE: (params: DELETECannedResponsesProps) => void; + }; + + '/v1/canned-responses/:_id': { + GET: () => { cannedResponse: IOmnichannelCannedResponse }; + }; + + '/v1/canned-responses.get': { + GET: () => { responses: IOmnichannelCannedResponse[] }; }; '/v1/livechat/webrtc.call': { - GET: (params: { rid: string }) => void; + GET: (params: GETWebRTCCall) => { videoCall: { rid: string; provider: string; callStatus: 'ringing' | 'ongoing' } }; }; '/v1/livechat/webrtc.call/:callId': { - PUT: (params: { rid: string; status: 'ended' }) => void; + PUT: (params: PUTWebRTCCallId) => { status: string | undefined }; }; '/v1/livechat/priorities': { @@ -1274,11 +2814,7 @@ export type OmnichannelEndpoints = { }; '/v1/livechat/visitors.search': { - GET: ( - params: PaginatedRequest<{ - term: string; - }>, - ) => PaginatedResult<{ visitors: any[] }>; + GET: (params: GETLivechatVisitorsSearch) => PaginatedResult<{ visitors: (ILivechatVisitor & { fname?: string })[] }>; }; '/v1/omnichannel/contact': { POST: (params: POSTOmnichannelContactProps) => { contact: string }; @@ -1289,4 +2825,265 @@ export type OmnichannelEndpoints = { '/v1/omnichannel/contact.search': { GET: (params: GETOmnichannelContactSearchProps) => { contact: ILivechatVisitor | null }; }; + '/v1/livechat/agent.info/:rid/:token': { + GET: () => { agent: ILivechatAgent }; + }; + '/v1/livechat/agent.next/:token': { + GET: (params: GETAgentNextToken) => { agent: ILivechatAgent } | void; + }; + '/v1/livechat/config': { + GET: (params: GETLivechatConfigParams) => { + config: { [k: string]: string | boolean } & { room?: IOmnichannelRoom; agent?: ILivechatAgent }; + }; + }; + '/v1/livechat/custom.field': { + POST: (params: POSTLivechatCustomFieldParams) => { field: { key: string; value: string; overwrite: boolean } }; + }; + '/v1/livechat/custom.fields': { + POST: (params: POSTLivechatCustomFieldsParams) => { fields: { Key: string; value: string; overwrite: boolean }[] }; + }; + '/v1/livechat/transfer.history/:rid': { + GET: () => PaginatedResult<{ history: Pick<IOmnichannelSystemMessage, 'transferData'>[] }>; + }; + '/v1/livechat/transcript': { + POST: (params: POSTLivechatTranscriptParams) => { message: string }; + }; + '/v1/livechat/offline.message': { + POST: (params: POSTLivechatOfflineMessageParams) => { message: string }; + }; + '/v1/livechat/page.visited': { + POST: (params: POSTLivechatPageVisitedParams) => { page: { msg: string; navigation: string } } | void; + }; + '/v1/livechat/message': { + POST: (params: POSTLivechatMessageParams) => { message: IMessage }; + }; + '/v1/livechat/message/:_id': { + GET: (parms: GETLivechatMessageIdParams) => { message: IMessage }; + PUT: (params: PUTLivechatMessageIdParams) => { message: IMessage }; + DELETE: (params: DELETELivechatMessageIdParams) => { message: Pick<Serialized<IMessage>, 'ts' | '_id'> }; + }; + '/v1/livechat/messages.history/:rid': { + GET: (params: GETLivechatMessagesHistoryRidParams) => { messages: IMessage[] }; + }; + '/v1/livechat/messages': { + POST: (params: GETLivechatMessagesParams) => { messages: { username: string; msg: string; ts: Date }[] }; + }; + '/v1/livechat/room': { + GET: (params: GETLivechatRoomParams) => { room: IOmnichannelRoom; newRoom: boolean } | IOmnichannelRoom; + }; + '/v1/livechat/room.close': { + POST: (params: POSTLivechatRoomCloseParams) => { rid: string; comment: string }; + }; + '/v1/livechat/room.transfer': { + POST: (params: POSTLivechatRoomTransferParams) => Deprecated<{ room: IOmnichannelRoom }>; + }; + '/v1/livechat/room.survey': { + POST: (params: POSTLivechatRoomSurveyParams) => { rid: string; data: unknown }; + }; + '/v1/livechat/room.visitor': { + PUT: (params: PUTLivechatRoomVisitorParams) => Deprecated<{ room: IOmnichannelRoom }>; + }; + '/v1/livechat/visitors.pagesVisited/:roomId': { + GET: (params: GETLivechatVisitorsPagesVisitedRoomIdParams) => PaginatedResult<{ pages: IMessage[] }>; + }; + '/v1/livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId': { + GET: (params: GETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams) => PaginatedResult<{ history: IOmnichannelRoom[] }>; + }; + '/v1/livechat/visitors.searchChats/room/:roomId/visitor/:visitorId': { + GET: (params: GETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams) => PaginatedResult<{ history: IOmnichannelRoom[] }>; + }; + '/v1/livechat/visitors.autocomplete': { + GET: (params: GETLivechatVisitorsAutocompleteParams) => { + items: (ILivechatVisitor & { + custom_name: string; + })[]; + }; + }; + '/v1/livechat/agents/:agentId/departments': { + GET: (params: GETLivechatAgentsAgentIdDepartmentsParams) => { departments: ILivechatDepartmentAgents[] }; + }; + '/v1/livechat/business-hour': { + GET: (params: GETBusinessHourParams) => { businessHour: ILivechatBusinessHour }; + }; + '/v1/livechat/triggers': { + GET: (params: GETLivechatTriggersParams) => { triggers: WithId<ILivechatTrigger>[] }; + }; + '/v1/livechat/triggers/:_id': { + GET: () => { trigger: ILivechatTrigger | null }; + }; + '/v1/livechat/rooms': { + GET: (params: GETLivechatRoomsParams) => PaginatedResult<{ rooms: IOmnichannelRoom[] }>; + }; + '/v1/livechat/queue': { + GET: (params: GETLivechatQueueParams) => PaginatedResult<{ + queue: { + _id: string; + user: { _id: string; userId: string; username: string; status: string }; + department: { _id: string; name: string }; + chats: number; + }[]; + }>; + }; + '/v1/livechat/integrations.settings': { + GET: () => { settings: ISetting[] }; + }; + '/v1/livechat/upload/:rid': { + POST: () => IMessage & { newRoom: boolean; showConnecting: boolean }; + }; + '/v1/livechat/facebook': { + POST: (params: POSTLivechatFacebookParams) => { message: IMessage }; + }; + '/v1/livechat/inquiries.list': { + GET: (params: GETLivechatInquiriesListParams) => PaginatedResult<{ inquiries: ILivechatInquiryRecord[] }>; + }; + '/v1/livechat/inquiries.take': { + POST: (params: POSTLivechatInquiriesTakeParams) => { inquiry: ILivechatInquiryRecord }; + }; + '/v1/livechat/inquiries.queued': { + GET: (params: GETLivechatInquiriesQueuedParams) => PaginatedResult<{ inquiries: ILivechatInquiryRecord[] }>; + }; + '/v1/livechat/inquiries.queuedForUser': { + GET: (params: GETLivechatInquiriesQueuedForUserParams) => PaginatedResult<{ inquiries: ILivechatInquiryRecord[] }>; + }; + '/v1/livechat/inquiries.getOne': { + GET: (params: GETLivechatInquiriesGetOneParams) => { inquiry: ILivechatInquiryRecord | null }; + }; + '/v1/livechat/analytics/dashboards/conversation-totalizers': { + GET: (params: GETDashboardTotalizers) => { + totalizers: { title: string; value: number }[]; + }; + }; + '/v1/livechat/analytics/dashboards/agents-productivity-totalizers': { + GET: (params: GETDashboardTotalizers) => { + totalizers: { title: string; value: number }[]; + }; + }; + '/v1/livechat/analytics/dashboards/chats-totalizers': { + GET: (params: GETDashboardTotalizers) => { + totalizers: { title: string; value: number }[]; + }; + }; + '/v1/livechat/analytics/dashboards/productivity-totalizers': { + GET: (params: GETDashboardTotalizers) => { + totalizers: { title: string; value: number }[]; + }; + }; + '/v1/livechat/analytics/dashboards/charts/chats': { + GET: (params: GETDashboardTotalizers) => { + open: number; + closed: number; + queued: number; + onhold: number; + }; + }; + '/v1/livechat/analytics/dashboards/charts/chats-per-agent': { + GET: (params: GETDashboardTotalizers) => { + [k: string]: { open: number; closed: number; onhold: number }; + }; + }; + '/v1/livechat/analytics/dashboards/charts/chats-per-department': { + GET: (params: GETDashboardTotalizers) => { + [k: string]: { open: number; closed: number }; + }; + }; + '/v1/livechat/analytics/dashboards/charts/timings': { + GET: (params: GETDashboardTotalizers) => { + response: { avg: number; longest: number }; + reaction: { avg: number; longest: number }; + chatDuration: { avg: number; longest: number }; + }; + }; + '/v1/livechat/analytics/dashboards/charts/agents-status': { + GET: (params: GETDashboardsAgentStatusParams) => { offline: number; away: number; busy: number; available: number }; + }; +} & { + // EE + '/v1/livechat/analytics/agents/average-service-time': { + GET: (params: LivechatAnalyticsAgentsAverageServiceTimeProps) => PaginatedResult<{ + agents: { + _id: string; + username: string; + name: string; + active: boolean; + averageServiceTimeInSeconds: number; + }[]; + }>; + }; + '/v1/livechat/analytics/agents/total-service-time': { + GET: (params: LivechatAnalyticsAgentsTotalServiceTimeProps) => PaginatedResult<{ + agents: { + _id: string; + username: string; + chats: number; + serviceTimeDuration: number; + }[]; + }>; + }; + '/v1/livechat/analytics/agents/available-for-service-history': { + GET: (params: LivechatAnalyticsAgentsAvailableForServiceHistoryProps) => PaginatedResult<{ + agents: ILivechatAgentActivity[]; + }>; + }; + '/v1/livechat/analytics/departments/amount-of-chats': { + GET: (params: LivechatAnalyticsDepartmentsAmountOfChatsProps) => PaginatedResult<{ + departments: IOmnichannelRoom[]; + }>; + }; + '/v1/livechat/analytics/departments/average-service-time': { + GET: (params: LivechatAnalyticsDepartmentsAverageServiceTimeProps) => PaginatedResult<{ + departments: { + _id: string; + averageServiceTimeInSeconds: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/average-chat-duration-time': { + GET: (params: LivechatAnalyticsDepartmentsAverageChatDurationTimeProps) => PaginatedResult<{ + departments: { + _id: string; + averageChatDurationTimeInSeconds: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/total-service-time': { + GET: (params: LivechatAnalyticsDepartmentsTotalServiceTimeProps) => PaginatedResult<{ + departments: { + _id: string; + serviceTimeDuration: number; + chats: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/average-waiting-time': { + GET: (params: LivechatAnalyticsDepartmentsAverageWaitingTimeProps) => PaginatedResult<{ + departments: { + _id: string; + averageWaitingTimeInSeconds: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/total-transferred-chats': { + GET: (params: LivechatAnalyticsDepartmentsTotalTransferredChatsProps) => PaginatedResult<{ + departments: { + _id: string; + numberOfTransferredRooms: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/total-abandoned-chats': { + GET: (params: LivechatAnalyticsDepartmentsTotalAbandonedChatsProps) => PaginatedResult<{ + departments: { + _id: string; + abandonedRooms: number; + }[]; + }>; + }; + '/v1/livechat/analytics/departments/percentage-abandoned-chats': { + GET: (params: LivechatAnalyticsDepartmentsPercentageAbandonedChatsProps) => PaginatedResult<{ + departments: { + _id: string; + percentageOfAbandonedRooms: number; + }[]; + }>; + }; }; diff --git a/yarn.lock b/yarn.lock index 6a7de33154d..e0b3511b067 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6237,7 +6237,7 @@ __metadata: fibers: ^5.0.1 file-type: ^16.5.3 filenamify: ^4.3.0 - filesize: ^3.6.1 + filesize: 9.0.11 google-libphonenumber: ^3.2.28 googleapis: ^104.0.0 grapheme-splitter: ^1.0.4 @@ -18798,10 +18798,10 @@ __metadata: languageName: node linkType: hard -"filesize@npm:^3.6.1": - version: 3.6.1 - resolution: "filesize@npm:3.6.1" - checksum: 9ba47e9df90cd6bb6c0434418123facf9dafbe92c850f29ed50bfa42d60d00f8501a8a9b962f77ec7d1ba30190d5dbda5f6f56c5e56bce9e09729988bf0613c4 +"filesize@npm:9.0.11": + version: 9.0.11 + resolution: "filesize@npm:9.0.11" + checksum: 7e8a9f9a4089e3ee29de64c241b50db8db4008351ace959873cb950bac0c2e1e09f4b45f42eaed8acd589a895dde978ed199e7a9982902fa06ca0be48e5ea92d languageName: node linkType: hard -- GitLab From 5ed3271f8344f947988e8002a02a939b528ab082 Mon Sep 17 00:00:00 2001 From: Douglas Fabris <devfabris@gmail.com> Date: Thu, 22 Sep 2022 14:46:40 -0300 Subject: [PATCH 058/107] [IMPROVE] VideoConference Messages UI (#26548) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> Co-authored-by: Pierre <pierre.lehnen@rocket.chat> --- .../client/views/blocks/MessageBlock.js | 39 +- apps/meteor/package.json | 10 +- .../rocketchat-i18n/i18n/en.i18n.json | 4 + .../server/models/raw/VideoConference.ts | 5 +- .../modules/listeners/listeners.module.ts | 6 + apps/meteor/server/sdk/lib/Events.ts | 2 + .../services/video-conference/service.ts | 225 ++--- ee/apps/ddp-streamer/package.json | 2 +- packages/agenda/package.json | 2 +- packages/api-client/package.json | 2 +- packages/cas-validate/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/core-typings/src/IVideoConference.ts | 2 +- packages/eslint-config/package.json | 2 +- packages/favicon/package.json | 2 +- packages/fuselage-ui-kit/package.json | 27 +- .../VideoConferenceBlock.tsx | 183 ++++ .../hooks/useVideoConfData.ts | 12 + .../hooks/useVideoConfDataStream.ts | 63 ++ .../src/blocks/VideoConferenceBlock/index.tsx | 3 + .../src/contexts/kitContext.ts | 1 + .../src/surfaces/FuselageSurfaceRenderer.tsx | 26 +- .../src/surfaces/MessageSurfaceRenderer.tsx | 41 + .../fuselage-ui-kit/src/surfaces/index.ts | 3 +- packages/gazzodown/package.json | 17 +- packages/livechat/package.json | 5 +- .../components/Messages/MessageList/index.js | 6 +- packages/livechat/src/lib/room.js | 4 +- packages/model-typings/package.json | 2 +- .../src/models/IVideoConferenceModel.ts | 2 +- packages/models/package.json | 2 +- packages/node-poplib/package.json | 2 +- packages/rest-typings/package.json | 2 +- packages/ui-client/package.json | 8 +- packages/ui-contexts/package.json | 5 +- packages/ui-video-conf/.eslintrc.json | 13 +- packages/ui-video-conf/.storybook/main.js | 12 + packages/ui-video-conf/.storybook/preview.js | 25 + packages/ui-video-conf/package.json | 20 +- .../VideoConfMessage.stories.tsx | 110 +++ .../src/VideoConfMessage/VideoConfMessage.tsx | 8 + .../VideoConfMessageAction.tsx | 15 + .../VideoConfMessageFooter.tsx | 14 + .../VideoConfMessageFooterText.tsx | 9 + .../VideoConfMessage/VideoConfMessageIcon.tsx | 39 + .../VideoConfMessage/VideoConfMessageRow.tsx | 6 + .../VideoConfMessageSkeleton.tsx | 18 + .../VideoConfMessage/VideoConfMessageText.tsx | 6 + .../VideoConfMessageUserStack.tsx | 16 + .../src/VideoConfMessage/index.ts | 21 + .../src/VideoConfPopup/VideoConfPopup.tsx | 2 +- packages/ui-video-conf/src/index.ts | 1 + yarn.lock | 841 +++++++----------- 53 files changed, 1127 insertions(+), 770 deletions(-) create mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx create mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts create mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts create mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx create mode 100644 packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx create mode 100644 packages/ui-video-conf/.storybook/main.js create mode 100644 packages/ui-video-conf/.storybook/preview.js create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx create mode 100644 packages/ui-video-conf/src/VideoConfMessage/index.ts diff --git a/apps/meteor/client/views/blocks/MessageBlock.js b/apps/meteor/client/views/blocks/MessageBlock.js index 1b9c194350c..965a0d9b395 100644 --- a/apps/meteor/client/views/blocks/MessageBlock.js +++ b/apps/meteor/client/views/blocks/MessageBlock.js @@ -1,12 +1,21 @@ import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket.chat/fuselage-ui-kit'; import React from 'react'; import * as ActionManager from '../../../app/ui-message/client/ActionManager'; import { useBlockRendered } from '../../components/message/hooks/useBlockRendered'; -import { useVideoConfJoinCall, useVideoConfSetPreferences } from '../../contexts/VideoConfContext'; +import { + useVideoConfJoinCall, + useVideoConfSetPreferences, + useVideoConfIsCalling, + useVideoConfIsRinging, + useVideoConfDispatchOutgoing, +} from '../../contexts/VideoConfContext'; +import { VideoConfManager } from '../../lib/VideoConfManager'; import { renderMessageBody } from '../../lib/utils/renderMessageBody'; import './textParsers'; +import { useVideoConfWarning } from '../room/contextualBar/VideoConference/useVideoConfWarning'; // TODO: move this to fuselage-ui-kit itself const mrkdwn = ({ text } = {}) => text && <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />; @@ -16,14 +25,36 @@ function MessageBlock({ mid: _mid, rid, blocks, appId }) { const { ref, className } = useBlockRendered(); const joinCall = useVideoConfJoinCall(); const setPreferences = useVideoConfSetPreferences(); + const isCalling = useVideoConfIsCalling(); + const isRinging = useVideoConfIsRinging(); + const dispatchWarning = useVideoConfWarning(); + const dispatchPopup = useVideoConfDispatchOutgoing(); + + const handleOpenVideoConf = useMutableCallback(async (rid) => { + if (isCalling || isRinging) { + return; + } + + try { + await VideoConfManager.loadCapabilities(); + dispatchPopup({ rid }); + } catch (error) { + dispatchWarning(error.error); + } + }); const context = { action: ({ actionId, value, blockId, mid = _mid, appId }, event) => { - if (appId === 'videoconf-core' && actionId === 'join') { + if (appId === 'videoconf-core') { event.preventDefault(); setPreferences({ mic: true, cam: false }); - joinCall(blockId); - return; + if (actionId === 'join') { + return joinCall(blockId); + } + + if (actionId === 'callBack') { + return handleOpenVideoConf(blockId); + } } ActionManager.triggerBlockAction({ diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 1396343f2c6..cd04236706c 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -60,11 +60,11 @@ "email": "support@rocket.chat" }, "devDependencies": { - "@babel/core": "^7.18.9", + "@babel/core": "^7.18.13", "@babel/eslint-parser": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/preset-env": "^7.18.9", + "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.18.6", "@babel/register": "^7.18.9", "@faker-js/faker": "^6.3.1", @@ -125,7 +125,7 @@ "@types/prometheus-gc-stats": "^0.6.2", "@types/proxyquire": "^1.3.28", "@types/psl": "^1.1.0", - "@types/react": "~17.0.47", + "@types/react": "~17.0.48", "@types/react-dom": "~17.0.17", "@types/rewire": "^2.5.28", "@types/semver": "^7.3.10", @@ -153,7 +153,7 @@ "chai-spies": "^1.0.0", "cross-env": "^7.0.3", "emojione-assets": "^4.5.0", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", @@ -211,7 +211,7 @@ "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/fuselage-ui-kit": "next", + "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/icons": "next", "@rocket.chat/layout": "next", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 94a66453c94..ac81f641efa 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -764,6 +764,7 @@ "By_author": "By __author__", "cache_cleared": "Cache cleared", "Call": "Call", + "Call_back": "Call back", "Calling": "Calling", "Call_Center": "Voice Channel", "Call_Center_Description": "Configure Rocket.Chat's voice channels", @@ -780,7 +781,9 @@ "Call_number_enterprise_only": "Call number (Enterprise Edition only)", "call-management": "Call Management", "call-management_description": "Permission to start a meeting", + "Call_ongoing": "Call ongoing", "Call_unavailable_for_federation": "Call is unavailable for Federated rooms", + "Call_was_not_answered": "Call was not answered", "Caller": "Caller", "Caller_Id": "Caller ID", "Cam_on": "Cam On", @@ -5232,6 +5235,7 @@ "VoIP_Toggle": "Enable/Disable VoIP", "Chat_opened_by_visitor": "Chat opened by the visitor", "Wait_activation_warning": "Before you can login, your account must be manually activated by an administrator.", + "Waiting_for_answer": "Waiting for answer", "Waiting_queue": "Waiting queue", "Waiting_queue_message": "Waiting queue message", "Waiting_queue_message_description": "Message that will be displayed to the visitors when they get in the queue", diff --git a/apps/meteor/server/models/raw/VideoConference.ts b/apps/meteor/server/models/raw/VideoConference.ts index 96d6cc7f311..4185ed2da50 100644 --- a/apps/meteor/server/models/raw/VideoConference.ts +++ b/apps/meteor/server/models/raw/VideoConference.ts @@ -189,7 +189,10 @@ export class VideoConferenceRaw extends BaseRaw<VideoConference> implements IVid }); } - public async addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void> { + public async addUserById( + callId: string, + user: Required<Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'>> & { ts?: Date }, + ): Promise<void> { await this.updateOneById(callId, { $addToSet: { users: { diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 3c1c330c5ec..1380756a17d 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -128,6 +128,12 @@ export class ListenersModule { notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); }); + service.onEvent('message.update', ({ message }) => { + if (message.rid) { + notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); + } + }); + service.onEvent('watch.subscriptions', ({ clientAction, subscription }) => { if (!subscription.u?._id) { return; diff --git a/apps/meteor/server/sdk/lib/Events.ts b/apps/meteor/server/sdk/lib/Events.ts index 6b4336d6d70..07872ed14fe 100644 --- a/apps/meteor/server/sdk/lib/Events.ts +++ b/apps/meteor/server/sdk/lib/Events.ts @@ -22,6 +22,7 @@ import type { IWebdavAccount, ICustomSound, VoipEventDataSignature, + AtLeast, UserStatus, } from '@rocket.chat/core-typings'; @@ -136,4 +137,5 @@ export type EventSignatures = { 'call.callerhangup'(userId: string, data: { roomId: string }): void; 'watch.pbxevents'(data: { clientAction: ClientAction; data: Partial<IPbxEvent>; id: string }): void; 'connector.statuschanged'(enabled: boolean): void; + 'message.update'(data: { message: AtLeast<IMessage, 'rid'> }): void; }; diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 0808e685ba5..c11b775b288 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -23,7 +23,7 @@ import { isGroupVideoConference, isLivechatVideoConference, } from '@rocket.chat/core-typings'; -import type { MessageSurfaceLayout, ContextBlock } from '@rocket.chat/ui-kit'; +import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; @@ -34,7 +34,6 @@ import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; import { Apps } from '../../../app/apps/server'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { settings } from '../../../app/settings/server'; -import { getURL } from '../../../app/utils/server'; import { videoConfProviders } from '../../lib/videoConfProviders'; import { videoConfTypes } from '../../lib/videoConfTypes'; import { updateCounter } from '../../../app/statistics/server/functions/updateStatsCounter'; @@ -147,10 +146,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } if (call.messages.started) { - const name = - (settings.get<boolean>('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; - const text = TAPi18n.__('video_direct_missed', { username: name }); - await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]); + this.updateVideoConfMessage(call.messages.started); } await VideoConferenceModel.setDataById(callId, { @@ -237,8 +233,8 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('Invalid User'); } - const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name'>>>(userId, { - projection: { username: 1, name: 1 }, + const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>>(userId, { + projection: { username: 1, name: 1, avatarETag: 1 }, }); if (!user) { throw new Error('Invalid User'); @@ -248,6 +244,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf _id: user._id, username: user.username, name: user.name, + avatarETag: user.avatarETag, ts: ts || new Date(), }); } @@ -372,6 +369,13 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return true; } + private async updateVideoConfMessage(messageId: IMessage['_id']): Promise<void> { + const message = await Messages.findOneById(messageId); + if (message) { + api.broadcast('message.update', { message }); + } + } + private async endCall(callId: VideoConference['_id']): Promise<void> { const call = await this.getUnfiltered(callId); if (!call) { @@ -380,12 +384,11 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.ENDED }); if (call.messages?.started) { - await this.removeJoinButton(call.messages.started); + this.updateVideoConfMessage(call.messages.started); } - switch (call.type) { - case 'direct': - return this.endDirectCall(call); + if (call.type === 'direct') { + return this.endDirectCall(call); } } @@ -396,16 +399,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.EXPIRED }); - if (call.messages?.started) { - return this.removeJoinButton(call.messages.started); - } - } - - private async removeJoinButton(messageId: IMessage['_id']): Promise<void> { - await Messages.removeVideoConfJoinButton(messageId); - - const text = TAPi18n.__('Conference_call_has_ended'); - await Messages.addBlocksById(messageId, [this.buildMessageBlock(text)]); } private async endDirectCall(call: IDirectVideoConference): Promise<void> { @@ -443,66 +436,21 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return videoConfTypes.getTypeForRoom(room, allowRinging); } - private async createMessage( - rid: IRoom['_id'], - providerName: string, - extraData: Partial<IMessage> = {}, - createdBy?: IUser, - ): Promise<IMessage['_id']> { + private async createMessage(call: VideoConference, createdBy?: IUser, customBlocks?: IMessage['blocks']): Promise<IMessage['_id']> { const record = { msg: '', groupable: false, - ...extraData, + blocks: customBlocks || [this.buildVideoConfBlock(call._id)], }; - const room = await Rooms.findOneById(rid); - const appId = videoConfProviders.getProviderAppId(providerName); + const room = await Rooms.findOneById(call.rid); + const appId = videoConfProviders.getProviderAppId(call.providerName); const user = createdBy || (appId && (await Users.findOneByAppId(appId))) || (await Users.findOneById('rocket.cat')); const message = sendMessage(user, record, room, false); return message._id; } - private async createDirectCallMessage(call: IDirectVideoConference, user: IUser): Promise<IMessage['_id']> { - const username = (settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; - const text = TAPi18n.__('video_direct_calling', { - username, - }); - - return this.createMessage( - call.rid, - call.providerName, - { - blocks: [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)], - }, - user, - ); - } - - private async createGroupCallMessage(call: IGroupVideoConference, user: IUser, useAppUser = true): Promise<IMessage['_id']> { - const username = (settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; - const text = TAPi18n.__(useAppUser ? 'video_conference_started_by' : 'video_conference_started', { - conference: call.title || '', - username, - }); - - return this.createMessage( - call.rid, - call.providerName, - { - blocks: [ - this.buildMessageBlock(text), - this.buildJoinButtonBlock(call._id, call.title), - { - type: 'context', - elements: [], - }, - ], - } as Partial<IMessage>, - useAppUser ? undefined : user, - ); - } - private async validateProvider(providerName: string): Promise<void> { const manager = await this.getProviderManager(); const configured = await manager.isFullyConfigured(providerName).catch(() => false); @@ -545,35 +493,37 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf username, }); - return this.createMessage( - call.rid, - call.providerName, + return this.createMessage(call, user, [ + this.buildMessageBlock(text), { - blocks: [ - this.buildMessageBlock(text), + type: 'actions', + appId: 'videoconf-core', + blockId: call._id, + elements: [ { - type: 'actions', appId: 'videoconf-core', blockId: call._id, - elements: [ - { - appId: 'videoconf-core', - blockId: call._id, - actionId: 'joinLivechat', - type: 'button', - text: { - type: 'plain_text', - text: TAPi18n.__('Join_call'), - emoji: true, - }, - url, - }, - ], + actionId: 'joinLivechat', + type: 'button', + text: { + type: 'plain_text', + text: TAPi18n.__('Join_call'), + emoji: true, + }, + url, }, ], }, - user, - ); + ]); + } + + private buildVideoConfBlock(callId: string): MessageSurfaceLayout[number] { + return { + type: 'video_conf', + blockId: callId, + callId, + appId: 'videoconf-core', + }; } private buildMessageBlock(text: string): MessageSurfaceLayout[number] { @@ -587,27 +537,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }; } - private buildJoinButtonBlock(callId: string, title = ''): MessageSurfaceLayout[number] { - return { - type: 'actions', - appId: 'videoconf-core', - elements: [ - { - appId: 'videoconf-core', - blockId: callId, - actionId: 'join', - value: title, - type: 'button', - text: { - type: 'plain_text', - text: TAPi18n.__('Join_call'), - emoji: true, - }, - }, - ], - }; - } - private async startDirect( providerName: string, user: IUser, @@ -638,7 +567,8 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const url = await this.generateNewUrl(call); VideoConferenceModel.setUrlById(callId, url); - const messageId = await this.createDirectCallMessage(call, user); + const messageId = await this.createMessage(call, user); + VideoConferenceModel.setMessageById(callId, 'started', messageId); // After 40 seconds if the status is still "calling", we cancel the call automatically. @@ -700,7 +630,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf call.url = url; - const messageId = await this.createGroupCallMessage(call, user, useAppUser); + const messageId = await this.createMessage(call, useAppUser ? undefined : user); VideoConferenceModel.setMessageById(callId, 'started', messageId); if (call.ringing) { @@ -732,6 +662,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const joinUrl = await this.getUrl(call); const messageId = await this.createLivechatMessage(call, user, joinUrl); + await VideoConferenceModel.setMessageById(callId, 'started', messageId); return { @@ -869,7 +800,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private async addUserToCall( call: Optional<VideoConference, 'providerData'>, - { _id, username, name, avatarETag, ts }: AtLeast<IUser, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date }, + { _id, username, name, avatarETag, ts }: AtLeast<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date }, ): Promise<void> { if (call.users.find((user) => user._id === _id)) { return; @@ -877,64 +808,13 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.addUserById(call._id, { _id, username, name, avatarETag, ts }); - switch (call.type) { - case 'videoconference': - return this.updateGroupCallMessage(call as IGroupVideoConference, { _id, username, name }); - case 'direct': - return this.updateDirectCall(call as IDirectVideoConference, _id); + if (call.type === 'direct') { + return this.updateDirectCall(call as IDirectVideoConference, _id); } } private async addAnonymousUser(call: Optional<IGroupVideoConference, 'providerData'>): Promise<void> { await VideoConferenceModel.increaseAnonymousCount(call._id); - - if (!call.messages.started) { - return; - } - - const imageUrl = getURL(`/avatar/@a`, { cdn: false, full: true }); - return this.addAvatarToCallMessage(call.messages.started, imageUrl, TAPi18n.__('Anonymous')); - } - - private async addAvatarToCallMessage(messageId: IMessage['_id'], imageUrl: string, altText: string): Promise<void> { - const message = await Messages.findOneById<Pick<IMessage, '_id' | 'blocks'>>(messageId, { projection: { blocks: 1 } }); - if (!message) { - return; - } - - const blocks = message.blocks || []; - - const avatarsBlock = (blocks.find((block) => block.type === 'context') || { type: 'context', elements: [] }) as ContextBlock; - if (!blocks.includes(avatarsBlock)) { - blocks.push(avatarsBlock); - } - - if (avatarsBlock.elements.find((el) => el.type === 'image' && el.imageUrl === imageUrl)) { - return; - } - - avatarsBlock.elements = [ - ...avatarsBlock.elements, - { - type: 'image', - imageUrl, - altText, - }, - ]; - - await Messages.setBlocksById(message._id, blocks); - } - - private async updateGroupCallMessage( - call: Optional<IGroupVideoConference, 'providerData'>, - user: Pick<IUser, '_id' | 'username' | 'name'>, - ): Promise<void> { - if (!call.messages.started || !user.username) { - return; - } - const imageUrl = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - - return this.addAvatarToCallMessage(call.messages.started, imageUrl, user.name || user.username); } private async updateDirectCall(call: IDirectVideoConference, newUserId: IUser['_id']): Promise<void> { @@ -955,10 +835,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.setStatusById(call._id, VideoConferenceStatus.STARTED); if (call.messages.started) { - const username = - (settings.get<boolean>('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; - const text = TAPi18n.__('video_direct_started', { username }); - await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)]); + this.updateVideoConfMessage(call.messages.started); } } } diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 98373c82dec..b315ac489c9 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -46,7 +46,7 @@ "@types/sharp": "^0.30.4", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3", - "eslint": "^8.21.0", + "eslint": "^8.22.0", "pino-pretty": "^7.6.1", "ts-node": "^10.9.1", "typescript": "~4.5.5" diff --git a/packages/agenda/package.json b/packages/agenda/package.json index 5ca16adef57..e2a0613c804 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/debug": "^4", "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 98f63afd69a..f9c9b1466a5 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@types/jest": "^27.4.1", "@types/strict-uri-encode": "^2.0.0", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "ts-jest": "^27.1.5", "typescript": "~4.5.5" diff --git a/packages/cas-validate/package.json b/packages/cas-validate/package.json index c8f23b455c7..22cc27646cd 100644 --- a/packages/cas-validate/package.json +++ b/packages/cas-validate/package.json @@ -5,7 +5,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 7faf5cf4e29..27ae9d5549c 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "mongodb": "^4.3.1", "prettier": "^2.7.1", "typescript": "~4.5.5" diff --git a/packages/core-typings/src/IVideoConference.ts b/packages/core-typings/src/IVideoConference.ts index a4c0b571347..ce6297c00cd 100644 --- a/packages/core-typings/src/IVideoConference.ts +++ b/packages/core-typings/src/IVideoConference.ts @@ -31,7 +31,7 @@ export type LivechatInstructions = { export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type']; -export interface IVideoConferenceUser extends Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> { +export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> { ts: Date; } diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index cc850fdaa0b..2e028655e6a 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -7,7 +7,7 @@ "@types/prettier": "^2.6.3", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-import": "^2.26.0", diff --git a/packages/favicon/package.json b/packages/favicon/package.json index 7dba2d92dad..a50c9aca6d8 100644 --- a/packages/favicon/package.json +++ b/packages/favicon/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "devDependencies": { - "eslint": "^8.20.0", + "eslint": "^8.22.0", "typescript": "~4.5.5" }, "scripts": { diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index b89ffc23b27..d11639351bf 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -38,13 +38,20 @@ "bump-next": "bump-next" }, "peerDependencies": { + "@rocket.chat/apps-engine": "*", + "@rocket.chat/eslint-config": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/fuselage-polyfills": "*", "@rocket.chat/icons": "*", + "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-kit": "*", + "@rocket.chat/ui-video-conf": "*", + "@tanstack/react-query": "*", + "react": "*", + "react-dom": "*" }, "devDependencies": { "@rocket.chat/apps-engine": "~1.30.0", @@ -55,6 +62,9 @@ "@rocket.chat/icons": "next", "@rocket.chat/prettier-config": "next", "@rocket.chat/styled": "next", + "@rocket.chat/ui-contexts": "workspace:^", + "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-video-conf": "workspace:^", "@storybook/addon-essentials": "~6.5.10", "@storybook/addons": "~6.5.10", "@storybook/builder-webpack5": "~6.5.10", @@ -62,23 +72,20 @@ "@storybook/react": "~6.5.10", "@storybook/source-loader": "~6.5.10", "@storybook/theming": "~6.5.10", - "@types/react": "~17.0.39", - "@types/react-dom": "^17.0.11", + "@tanstack/react-query": "^4.2.1", + "@types/react": "~17.0.48", + "@types/react-dom": "~17.0.17", "babel-loader": "~8.2.3", "cross-env": "^7.0.3", - "eslint": "~8.8.0", + "eslint": "~8.22.0", "lint-staged": "~12.3.3", "normalize.css": "^8.0.1", "npm-run-all": "^4.1.5", "prettier": "~2.5.1", - "react": "^17.0.2", "react-dom": "^17.0.2", "rimraf": "^3.0.2", + "tslib": "^2.3.1", "typescript": "~4.3.5", "webpack": "~5.68.0" - }, - "dependencies": { - "@rocket.chat/ui-kit": "next", - "tslib": "^2.3.1" } } diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx new file mode 100644 index 00000000000..d62d613dcd1 --- /dev/null +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx @@ -0,0 +1,183 @@ +import type * as UiKit from '@rocket.chat/ui-kit'; +import { useTranslation, useUserAvatarPath } from '@rocket.chat/ui-contexts'; +import { Avatar } from '@rocket.chat/fuselage'; +import { + VideoConfMessageSkeleton, + VideoConfMessage, + VideoConfMessageRow, + VideoConfMessageIcon, + VideoConfMessageText, + VideoConfMessageFooter, + VideoConfMessageAction, + VideoConfMessageUserStack, + VideoConfMessageFooterText, +} from '@rocket.chat/ui-video-conf'; +import type { MouseEventHandler, ReactElement } from 'react'; +import React, { useContext, memo } from 'react'; + +import { useSurfaceType } from '../../contexts/SurfaceContext'; +import type { BlockProps } from '../../utils/BlockProps'; +import { useVideoConfDataStream } from './hooks/useVideoConfDataStream'; +import { kitContext } from '../..'; + +type VideoConferenceBlockProps = BlockProps<UiKit.VideoConferenceBlock>; + +const MAX_USERS = 6; + +const VideoConferenceBlock = ({ + block, +}: VideoConferenceBlockProps): ReactElement => { + const t = useTranslation(); + const { callId, appId = 'videoconf-core' } = block; + const surfaceType = useSurfaceType(); + + const { action, viewId, rid } = useContext(kitContext); + + if (surfaceType !== 'message') { + return <></>; + } + + if (!callId || !rid) { + return <></>; + } + + const getUserAvatarPath = useUserAvatarPath(); + const result = useVideoConfDataStream({ rid, callId }); + + const joinHandler: MouseEventHandler<HTMLButtonElement> = (e): void => { + action( + { + blockId: block.blockId || '', + appId, + actionId: 'join', + value: block.blockId || '', + viewId, + }, + e + ); + }; + + const callAgainHandler: MouseEventHandler<HTMLButtonElement> = (e): void => { + action( + { + blockId: rid || '', + appId, + actionId: 'callBack', + value: rid || '', + viewId, + }, + e + ); + }; + + if (result.isSuccess) { + const { data } = result; + + if ('endedAt' in data) { + return ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon /> + <VideoConfMessageText>{t('Call_ended')}</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + {data.type === 'direct' && ( + <> + <VideoConfMessageAction onClick={callAgainHandler}> + {t('Call_back')} + </VideoConfMessageAction> + <VideoConfMessageFooterText> + {t('Call_was_not_answered')} + </VideoConfMessageFooterText> + </> + )} + {data.type !== 'direct' && + (data.users.length ? ( + <> + <VideoConfMessageUserStack> + {data.users.map(({ username }, index) => + data.users.length <= MAX_USERS ? ( + <Avatar + size='x28' + key={index} + data-tooltip={username} + url={getUserAvatarPath(username as string)} + /> + ) : ( + <></> + ) + )} + </VideoConfMessageUserStack> + <VideoConfMessageFooterText> + {data.users.length > 6 + ? `+ ${MAX_USERS - data.users.length} ${t('Joined')}` + : t('Joined')} + </VideoConfMessageFooterText> + </> + ) : ( + <VideoConfMessageFooterText> + {t('Call_was_not_answered')} + </VideoConfMessageFooterText> + ))} + </VideoConfMessageFooter> + </VideoConfMessage> + ); + } + + if (data.type === 'direct' && data.status === 0) { + return ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon variant='incoming' /> + <VideoConfMessageText>{t('Calling')}</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageAction primary onClick={joinHandler}> + {t('Join')} + </VideoConfMessageAction> + <VideoConfMessageFooterText> + {t('Waiting_for_answer')} + </VideoConfMessageFooterText> + </VideoConfMessageFooter> + </VideoConfMessage> + ); + } + + return ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon variant='outgoing' /> + <VideoConfMessageText>{t('Call_ongoing')}</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageAction primary onClick={joinHandler}> + {t('Join')} + </VideoConfMessageAction> + <VideoConfMessageUserStack> + {data.users.map(({ username }, index) => + data.users.length <= MAX_USERS ? ( + <Avatar + size='x28' + key={index} + data-tooltip={username} + url={getUserAvatarPath(username as string)} + /> + ) : ( + <></> + ) + )} + </VideoConfMessageUserStack> + <VideoConfMessageFooterText> + {data.users.length > 6 + ? `+ ${MAX_USERS - data.users.length} ${t('Joined')}` + : t('Joined')} + </VideoConfMessageFooterText> + </VideoConfMessageFooter> + </VideoConfMessage> + ); + } + + return <VideoConfMessageSkeleton />; +}; + +export default memo(VideoConferenceBlock); diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts new file mode 100644 index 00000000000..ddbc0861698 --- /dev/null +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; + +export const useVideoConfData = ({ callId }: { callId: string }) => { + const videoConf = useEndpoint('GET', '/v1/video-conference.info'); + + return useQuery(['video-conference', callId], () => videoConf({ callId }), { + staleTime: Infinity, + refetchOnWindowFocus: false, + // refetchOnMount: false, + }); +}; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts new file mode 100644 index 00000000000..457d645d757 --- /dev/null +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts @@ -0,0 +1,63 @@ +import { useStream } from '@rocket.chat/ui-contexts'; +import { Emitter } from '@rocket.chat/emitter'; +import { useCallback, useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +import { useVideoConfData } from './useVideoConfData'; + +const ee = new Emitter(); + +const events = new Map(); + +const useStreamBySubPath = ( + streamer: ReturnType<typeof useStream>, + subpath: string, + callback: () => void +) => { + useEffect(() => { + if (!ee.has(subpath)) { + events.set( + subpath, + streamer(subpath, (...args) => { + ee.emit(subpath, ...args); + }) + ); + } + + return () => { + if (!ee.has(subpath)) { + events.delete(subpath); + } + }; + }, [streamer, subpath, callback]); + + useEffect(() => { + ee.on(subpath, callback); + + return () => { + ee.off(subpath, callback); + }; + }, [callback, subpath]); +}; + +export const useVideoConfDataStream = ({ + rid, + callId, +}: { + rid: string; + callId: string; +}) => { + const queryClient = useQueryClient(); + const subpath = `${rid}/${callId}`; + + const subscribeNotifyRoom = useStream('notify-room'); + + useStreamBySubPath( + subscribeNotifyRoom, + subpath, + useCallback(() => { + queryClient.invalidateQueries(['video-conference', callId]); + }, [subpath]) + ); + return useVideoConfData({ callId }); +}; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx new file mode 100644 index 00000000000..bbd942c7d3f --- /dev/null +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx @@ -0,0 +1,3 @@ +import VideoConferenceBlock from './VideoConferenceBlock'; + +export default VideoConferenceBlock; diff --git a/packages/fuselage-ui-kit/src/contexts/kitContext.ts b/packages/fuselage-ui-kit/src/contexts/kitContext.ts index b0cb4141b16..6eebb8cea7a 100644 --- a/packages/fuselage-ui-kit/src/contexts/kitContext.ts +++ b/packages/fuselage-ui-kit/src/contexts/kitContext.ts @@ -23,6 +23,7 @@ type UiKitContext = { errors?: Record<string, string>; values: Record<string, { value: string } | undefined>; viewId?: string; + rid?: string; }; export const defaultContext = { diff --git a/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx index f548f63ac67..fe2e61ee590 100644 --- a/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx +++ b/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx @@ -18,17 +18,23 @@ import OverflowElement from '../elements/OverflowElement'; import PlainTextInputElement from '../elements/PlainTextInputElement'; import StaticSelectElement from '../elements/StaticSelectElement'; +type FuselageSurfaceRendererProps = ConstructorParameters< + typeof UiKit.SurfaceRenderer +>[0]; + export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer<ReactElement> { - public constructor() { - super([ - 'actions', - 'context', - 'divider', - 'image', - 'input', - 'section', - 'preview', - ]); + public constructor(allowedBlocks?: FuselageSurfaceRendererProps) { + super( + allowedBlocks || [ + 'actions', + 'context', + 'divider', + 'image', + 'input', + 'section', + 'preview', + ] + ); } public plain_text( diff --git a/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx new file mode 100644 index 00000000000..6d83d774a9e --- /dev/null +++ b/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx @@ -0,0 +1,41 @@ +import * as UiKit from '@rocket.chat/ui-kit'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import VideoConferenceBlock from '../blocks/VideoConferenceBlock/VideoConferenceBlock'; +import { FuselageSurfaceRenderer } from './FuselageSurfaceRenderer'; + +export class FuselageMessageSurfaceRenderer extends FuselageSurfaceRenderer { + public constructor() { + super([ + 'actions', + 'context', + 'divider', + 'image', + 'input', + 'section', + 'preview', + 'video_conf', + ]); + } + + video_conf( + block: UiKit.VideoConferenceBlock, + context: UiKit.BlockContext, + index: number + ): ReactElement | null { + if (context === UiKit.BlockContext.BLOCK) { + return ( + <VideoConferenceBlock + key={index} + block={block} + context={context} + index={index} + surfaceRenderer={this} + /> + ); + } + + return null; + } +} diff --git a/packages/fuselage-ui-kit/src/surfaces/index.ts b/packages/fuselage-ui-kit/src/surfaces/index.ts index dd291834359..49eff1a923a 100644 --- a/packages/fuselage-ui-kit/src/surfaces/index.ts +++ b/packages/fuselage-ui-kit/src/surfaces/index.ts @@ -3,10 +3,11 @@ import { FuselageSurfaceRenderer } from './FuselageSurfaceRenderer'; import MessageSurface from './MessageSurface'; import ModalSurface from './ModalSurface'; import { createSurfaceRenderer } from './createSurfaceRenderer'; +import { FuselageMessageSurfaceRenderer } from './MessageSurfaceRenderer'; // export const attachmentParser = new FuselageSurfaceRenderer(); export const bannerParser = new FuselageSurfaceRenderer(); -export const messageParser = new FuselageSurfaceRenderer(); +export const messageParser = new FuselageMessageSurfaceRenderer(); export const modalParser = new FuselageSurfaceRenderer(); // export const UiKitAttachment = createSurfaceRenderer(AttachmentSurface, attachmentParser); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b1e9ce93dfb..1e5569f1bc2 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -3,11 +3,10 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/core": "^7.18.9", + "@babel/core": "~7.18.13", "@mdx-js/react": "^1.6.22", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/styled": "next", @@ -27,13 +26,13 @@ "@types/babel__core": "^7", "@types/jest": "^27.4.1", "@types/katex": "~0", - "@types/react": "^17.0.47", - "@types/react-dom": "^18", + "@types/react": "~17.0.48", + "@types/react-dom": "~17.0.17", "@types/testing-library__jest-dom": "^5", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", "babel-loader": "^8.2.5", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -61,16 +60,16 @@ "/dist" ], "peerDependencies": { - "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/core-typings": "*", "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "workspace:^", - "@rocket.chat/ui-contexts": "workspace:^", + "@rocket.chat/ui-client": "*", + "@rocket.chat/ui-contexts": "*", "katex": "*", - "react": "~17.0.2" + "react": "*" }, "dependencies": { "highlight.js": "^11.5.1", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index d87419d4658..bf9f8e56a3c 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.18.9", - "@babel/preset-env": "^7.18.9", + "@babel/preset-env": "^7.18.10", "@rocket.chat/eslint-config": "^0.4.0", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/logo": "next", @@ -36,7 +36,6 @@ "@storybook/addon-viewport": "~6.5.10", "@storybook/react": "~6.5.10", "@storybook/theming": "~6.5.10", - "@types/react-dom": "^17.0.17", "autoprefixer": "^9.8.8", "babel-loader": "^8.1.0", "babel-plugin-jsx-pragmatic": "^1.0.2", @@ -44,7 +43,7 @@ "css-loader": "^4.3.0", "cssnano": "^4.1.11", "desvg-loader": "^0.1.0", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", diff --git a/packages/livechat/src/components/Messages/MessageList/index.js b/packages/livechat/src/components/Messages/MessageList/index.js index 7ac2dbf60ab..f7ab458c017 100644 --- a/packages/livechat/src/components/Messages/MessageList/index.js +++ b/packages/livechat/src/components/Messages/MessageList/index.js @@ -101,7 +101,9 @@ export class MessageList extends MemoizedComponent { isVideoConfMessage(message) { return Boolean( - message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'), + message.blocks + ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') + ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'), ); } @@ -137,7 +139,7 @@ export class MessageList extends MemoizedComponent { } const videoConfJoinBlock = message.blocks - ?.find(({ appId }) => appId === 'videoconf-core') + ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); if (videoConfJoinBlock) { // If the call is not accepted yet, don't render the message. diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index d835b458cb7..c33d29ce89f 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -35,7 +35,9 @@ export const closeChat = async ({ transcriptRequested } = {}) => { }; const getVideoConfMessageData = (message) => - message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); + message.blocks + ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') + ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); const isVideoCallMessage = (message) => { if (message.t === constants.webRTCCallStartedMessageType) { diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 55516d3bb65..024d295148f 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@types/jest": "^27.4.1", "@types/node-rsa": "^1.1.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "ts-jest": "^27.1.5", diff --git a/packages/model-typings/src/models/IVideoConferenceModel.ts b/packages/model-typings/src/models/IVideoConferenceModel.ts index d1cfbcd919b..e0a1090eb5c 100644 --- a/packages/model-typings/src/models/IVideoConferenceModel.ts +++ b/packages/model-typings/src/models/IVideoConferenceModel.ts @@ -54,7 +54,7 @@ export interface IVideoConferenceModel extends IBaseModel<VideoConference> { setProviderDataById(callId: string, providerData: Record<string, any> | undefined): Promise<void>; - addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void>; + addUserById(callId: string, user: Required<Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'>> & { ts?: Date }): Promise<void>; setMessageById(callId: string, messageType: keyof VideoConference['messages'], messageId: string): Promise<void>; diff --git a/packages/models/package.json b/packages/models/package.json index 77846eb6425..6c6773d02ce 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "ts-jest": "^27.1.5", "typescript": "~4.5.5" diff --git a/packages/node-poplib/package.json b/packages/node-poplib/package.json index eac15f26ef1..792fc9f6f30 100644 --- a/packages/node-poplib/package.json +++ b/packages/node-poplib/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 0b8648c0d62..603c229976c 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "ts-jest": "^27.1.5", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index ce17f2da0d9..39aaa9d9cb6 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@babel/core": "~7.18.13", "@rocket.chat/css-in-js": "next", "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "next", @@ -18,10 +19,12 @@ "@storybook/manager-webpack4": "~6.5.10", "@storybook/react": "~6.5.10", "@storybook/testing-library": "~0.0.13", + "@types/babel__core": "^7", "@types/jest": "^27.4.1", "@types/postcss-url": "^10", - "@types/react": "~17.0.47", - "eslint": "^8.20.0", + "@types/react": "~17.0.48", + "@types/react-dom": "~17.0.17", + "eslint": "^8.22.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -56,6 +59,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", + "@rocket.chat/ui-contexts": "*", "react": "~17.0.2" } } diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 7858794225d..7bfd1f95fdc 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -8,9 +8,10 @@ "@rocket.chat/fuselage-hooks": "next", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "^27.4.1", - "@types/react": "^17.0.47", + "@types/react": "~17.0.48", + "@types/react-dom": "~17.0.17", "@types/use-sync-external-store": "^0.0.3", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "react": "~17.0.2", diff --git a/packages/ui-video-conf/.eslintrc.json b/packages/ui-video-conf/.eslintrc.json index 1859ff825f6..07037ccf0bc 100644 --- a/packages/ui-video-conf/.eslintrc.json +++ b/packages/ui-video-conf/.eslintrc.json @@ -5,7 +5,8 @@ "@rocket.chat/eslint-config/original", "prettier", "plugin:anti-trojan-source/recommended", - "plugin:react/jsx-runtime" + "plugin:react/jsx-runtime", + "plugin:storybook/recommended" ], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], @@ -63,5 +64,13 @@ "version": "detect" } }, - "ignorePatterns": ["**/dist"] + "ignorePatterns": ["**/dist"], + "overrides": [ + { + "files": ["*.stories.tsx"], + "rules": { + "react/no-multi-comp": "off" + } + } + ] } diff --git a/packages/ui-video-conf/.storybook/main.js b/packages/ui-video-conf/.storybook/main.js new file mode 100644 index 00000000000..4b6ae93751a --- /dev/null +++ b/packages/ui-video-conf/.storybook/main.js @@ -0,0 +1,12 @@ +module.exports = { + "stories": [ + "../src/**/*.stories.mdx", + "../src/**/*.stories.@(js|jsx|ts|tsx)" + ], + "addons": [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions" + ], + "framework": "@storybook/react" +} diff --git a/packages/ui-video-conf/.storybook/preview.js b/packages/ui-video-conf/.storybook/preview.js new file mode 100644 index 00000000000..abd704f7951 --- /dev/null +++ b/packages/ui-video-conf/.storybook/preview.js @@ -0,0 +1,25 @@ +import '../../../apps/meteor/app/theme/client/main.css'; +import 'highlight.js/styles/github.css'; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +} + +export const decorators = [ + (Story) => ( + <div className="rc-old"> + <style>{` + body { + background-color: white; + } + `}</style> + <Story /> + </div> + ) +]; diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7bab37906fa..4603e2340ab 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -3,15 +3,26 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@babel/core": "~7.18.13", "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/icons": "next", "@rocket.chat/styled": "next", + "@storybook/addon-actions": "~6.5.10", + "@storybook/addon-docs": "~6.5.10", + "@storybook/addon-essentials": "~6.5.10", + "@storybook/builder-webpack4": "~6.5.10", + "@storybook/manager-webpack4": "~6.5.10", + "@storybook/react": "~6.5.10", + "@storybook/testing-library": "~0.0.13", + "@types/babel__core": "^7", "@types/jest": "^27.4.1", - "eslint": "^8.20.0", + "eslint": "^8.22.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-storybook": "^0.6.4", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" @@ -20,6 +31,7 @@ "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", + "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", "react": "^17.0.2", "react-dom": "^17.0.2" @@ -29,11 +41,15 @@ "eslint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "jest": "jest", "build": "tsc -p tsconfig.json", + "storybook": "start-storybook -p 6006", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ "/dist" - ] + ], + "dependencies": { + "@rocket.chat/emitter": "next" + } } diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx new file mode 100644 index 00000000000..b253f28d949 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx @@ -0,0 +1,110 @@ +import { MessageDivider, Message, Avatar, Box } from '@rocket.chat/fuselage'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ReactElement } from 'react'; + +import '@rocket.chat/icons/dist/rocketchat.css'; +import { VideoConfMessage, VideoConfMessageIcon, VideoConfMessageRow, VideoConfMessageText } from '.'; +import VideoConfMessageAction from './VideoConfMessageAction'; +import VideoConfMessageFooter from './VideoConfMessageFooter'; +import VideoConfMessageFooterText from './VideoConfMessageFooterText'; +import VideoConfMessageSkeleton from './VideoConfMessageSkeleton'; +import VideoConfMessageUserStack from './VideoConfMessageUserStack'; + +export default { + title: 'Components/VideoConfMessage', + component: VideoConfMessage, + decorators: [ + (Story): ReactElement => ( + <Box> + <MessageDivider>May, 24, 2020</MessageDivider> + <Message className='customclass'> + <Message.LeftContainer> + <Avatar url={avatarUrl} size={'x36'} /> + </Message.LeftContainer> + <Message.Container> + <Message.Header> + <Message.Name>Haylie George</Message.Name> + <Message.Username>@haylie.george</Message.Username> + <Message.Role>Admin</Message.Role> + <Message.Role>User</Message.Role> + <Message.Role>Owner</Message.Role> + <Message.Timestamp>12:00 PM</Message.Timestamp> + </Message.Header> + <Message.Body> + <Story /> + </Message.Body> + </Message.Container> + </Message> + </Box> + ), + ], +} as ComponentMeta<typeof VideoConfMessage>; + +const avatarUrl = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; + +export const CallingDM: ComponentStory<typeof VideoConfMessage> = () => ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon variant='incoming' /> + <VideoConfMessageText>Calling...</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageAction primary>Join</VideoConfMessageAction> + <VideoConfMessageFooterText>Waiting for answer</VideoConfMessageFooterText> + </VideoConfMessageFooter> + </VideoConfMessage> +); + +export const CallEndedDM: ComponentStory<typeof VideoConfMessage> = () => ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon /> + <VideoConfMessageText>Call ended</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageAction>Call Back</VideoConfMessageAction> + <VideoConfMessageFooterText>Call was not answered</VideoConfMessageFooterText> + </VideoConfMessageFooter> + </VideoConfMessage> +); + +export const CallOngoing: ComponentStory<typeof VideoConfMessage> = () => ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon variant='outgoing' /> + <VideoConfMessageText>Call ongoing</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageAction primary>Join</VideoConfMessageAction> + <VideoConfMessageUserStack> + {Array(3) + .fill('') + .map((_, index) => ( + <Avatar key={index} size='x28' url={avatarUrl} /> + ))} + </VideoConfMessageUserStack> + <VideoConfMessageFooterText>Joined</VideoConfMessageFooterText> + </VideoConfMessageFooter> + </VideoConfMessage> +); + +export const CallEnded: ComponentStory<typeof VideoConfMessage> = () => ( + <VideoConfMessage> + <VideoConfMessageRow> + <VideoConfMessageIcon /> + <VideoConfMessageText>Call ended</VideoConfMessageText> + </VideoConfMessageRow> + <VideoConfMessageFooter> + <VideoConfMessageUserStack> + {Array(3) + .fill('') + .map((_, index) => ( + <Avatar key={index} size='x28' url={avatarUrl} /> + ))} + </VideoConfMessageUserStack> + </VideoConfMessageFooter> + </VideoConfMessage> +); + +export const Loading: ComponentStory<typeof VideoConfMessage> = () => <VideoConfMessageSkeleton />; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx new file mode 100644 index 00000000000..0aae5a21ee5 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +const VideoConfMessage = ({ ...props }): ReactElement => ( + <Box mbs='x4' maxWidth='345px' borderWidth={2} borderColor='neutral-200' borderRadius='x4' {...props} /> +); + +export default VideoConfMessage; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx new file mode 100644 index 00000000000..49e662833e0 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx @@ -0,0 +1,15 @@ +import { Box, Button } from '@rocket.chat/fuselage'; +import type { AllHTMLAttributes, ReactElement, ReactNode } from 'react'; + +const VideoConfMessageAction = ({ + children, + primary, + ...props +}: { children: ReactNode; primary?: boolean } & Omit<AllHTMLAttributes<HTMLButtonElement>, 'is'>): ReactElement => ( + <Box mi='x4'> + <Button small primary={primary} {...props}> + {children} + </Button> + </Box> +); +export default VideoConfMessageAction; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx new file mode 100644 index 00000000000..1adfc10820b --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx @@ -0,0 +1,14 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement, ReactNode } from 'react'; + +import VideoConfMessageRow from './VideoConfMessageRow'; + +const VideoConfMessageFooter = ({ children, ...props }: { children: ReactNode }): ReactElement => ( + <VideoConfMessageRow backgroundColor='neutral-100' {...props}> + <Box mi='neg-x4' display='flex' alignItems='center'> + {children} + </Box> + </VideoConfMessageRow> +); + +export default VideoConfMessageFooter; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx new file mode 100644 index 00000000000..975df374ff0 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx @@ -0,0 +1,9 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement, ReactNode } from 'react'; + +const VideoConfMessageFooterText = ({ children }: { children: ReactNode }): ReactElement => ( + <Box fontScale='c1' mi='x4'> + {children} + </Box> +); +export default VideoConfMessageFooterText; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx new file mode 100644 index 00000000000..7cec0601e77 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx @@ -0,0 +1,39 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ReactElement, ComponentProps } from 'react'; + +type VideoConfMessageIconProps = { + variant?: keyof typeof styles; +}; + +const styles = { + ended: { + icon: 'phone-off', + color: 'neutral-700', + backgroundColor: 'neutral-400', + }, + incoming: { + icon: 'phone-in', + color: 'primary-600', + backgroundColor: 'primary-200', + }, + outgoing: { + icon: 'phone', + color: 'success-800', + backgroundColor: 'success-200', + }, +} as const; + +const VideoConfMessageIcon = ({ variant = 'ended' }: VideoConfMessageIconProps): ReactElement => ( + <Box + size='x28' + alignItems='center' + justifyContent='center' + display='flex' + borderRadius='x4' + backgroundColor={styles[variant].backgroundColor} + > + <Icon size='x20' name={styles[variant].icon} color={styles[variant].color} /> + </Box> +); + +export default VideoConfMessageIcon; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx new file mode 100644 index 00000000000..b9dd67814f8 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx @@ -0,0 +1,6 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +const VideoConfMessageRow = ({ ...props }): ReactElement => <Box p='x16' display='flex' alignItems='center' {...props} />; + +export default VideoConfMessageRow; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx new file mode 100644 index 00000000000..18134774eb5 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +import VideoConfMessage from './VideoConfMessage'; +import VideoConfMessageRow from './VideoConfMessageRow'; + +const VideoConfMessageSkeleton = (): ReactElement => ( + <VideoConfMessage> + <VideoConfMessageRow> + <Skeleton width='full' pb='x4' /> + </VideoConfMessageRow> + <VideoConfMessageRow backgroundColor='neutral-100'> + <Skeleton width='full' pb='x4' /> + </VideoConfMessageRow> + </VideoConfMessage> +); + +export default VideoConfMessageSkeleton; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx new file mode 100644 index 00000000000..d0cfc3eafce --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx @@ -0,0 +1,6 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement, ComponentProps } from 'react'; + +const VideoConfMessageText = ({ ...props }: ComponentProps<typeof Box>): ReactElement => <Box fontScale='c2' mis='x8' {...props} />; + +export default VideoConfMessageText; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx new file mode 100644 index 00000000000..c555ddb5dc9 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx @@ -0,0 +1,16 @@ +import { Box } from '@rocket.chat/fuselage'; +import { Children, ReactElement } from 'react'; + +const VideoConfMessageUserStack = ({ children }: { children: ReactElement | ReactElement[] }): ReactElement => ( + <Box mi='x4'> + <Box display='flex' alignItems='center' mi='neg-x2'> + {Children.map(Children.toArray(children), (child, index) => ( + <Box mi='x2' key={index}> + {child} + </Box> + ))} + </Box> + </Box> +); + +export default VideoConfMessageUserStack; diff --git a/packages/ui-video-conf/src/VideoConfMessage/index.ts b/packages/ui-video-conf/src/VideoConfMessage/index.ts new file mode 100644 index 00000000000..a76957e92b2 --- /dev/null +++ b/packages/ui-video-conf/src/VideoConfMessage/index.ts @@ -0,0 +1,21 @@ +import VideoConfMessage from './VideoConfMessage'; +import VideoConfMessageAction from './VideoConfMessageAction'; +import VideoConfMessageFooter from './VideoConfMessageFooter'; +import VideoConfMessageFooterText from './VideoConfMessageFooterText'; +import VideoConfMessageIcon from './VideoConfMessageIcon'; +import VideoConfMessageRow from './VideoConfMessageRow'; +import VideoConfMessageSkeleton from './VideoConfMessageSkeleton'; +import VideoConfMessageText from './VideoConfMessageText'; +import VideoConfMessageUserStack from './VideoConfMessageUserStack'; + +export { + VideoConfMessage, + VideoConfMessageIcon, + VideoConfMessageRow, + VideoConfMessageText, + VideoConfMessageSkeleton, + VideoConfMessageFooter, + VideoConfMessageAction, + VideoConfMessageUserStack, + VideoConfMessageFooterText, +}; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx index 3f93b1a278b..be567f49469 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx @@ -22,7 +22,7 @@ const VideoConfPopup = forwardRef(function VideoConfPopup( ): ReactElement { return ( <VideoConfPopupContainer ref={ref} position={position}> - <Box p='x24' maxWidth='x276' backgroundColor='white'> + <Box p='x24' maxWidth='x276' borderRadius='x4' backgroundColor='white'> {children} </Box> </VideoConfPopupContainer> diff --git a/packages/ui-video-conf/src/index.ts b/packages/ui-video-conf/src/index.ts index e7244b55b21..d331731d0f4 100644 --- a/packages/ui-video-conf/src/index.ts +++ b/packages/ui-video-conf/src/index.ts @@ -1,6 +1,7 @@ import VideoConfButton from './VideoConfButton'; import VideoConfController from './VideoConfController'; +export * from './VideoConfMessage'; export * from './VideoConfPopup'; export * from './hooks'; export { VideoConfButton, VideoConfController }; diff --git a/yarn.lock b/yarn.lock index e0b3511b067..f7423fcc207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,13 +53,6 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.13.11, @babel/compat-data@npm:^7.18.8": - version: 7.18.8 - resolution: "@babel/compat-data@npm:7.18.8" - checksum: 3096aafad74936477ebdd039bcf342fba84eb3100e608f3360850fb63e1efa1c66037c4824f814d62f439ab47d25164439343a6e92e9b4357024fdf571505eb9 - languageName: node - linkType: hard - "@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.19.1": version: 7.19.1 resolution: "@babel/compat-data@npm:7.19.1" @@ -67,6 +60,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.18.8": + version: 7.18.13 + resolution: "@babel/compat-data@npm:7.18.13" + checksum: 869a730dc3ec40d4d5141b832d50b16702a2ea7bf5b87dc2761e7dfaa8deeafa03b8809fc42ff713ac1d450748dcdb07e1eb21f4633e10b87fd47be0065573e6 + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -91,20 +91,20 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": version: 7.18.9 resolution: "@babel/core@npm:7.18.9" dependencies: "@ampproject/remapping": ^2.1.0 "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.18.9 + "@babel/generator": ^7.18.13 "@babel/helper-compilation-targets": ^7.18.9 "@babel/helper-module-transforms": ^7.18.9 "@babel/helpers": ^7.18.9 - "@babel/parser": ^7.18.9 - "@babel/template": ^7.18.6 - "@babel/traverse": ^7.18.9 - "@babel/types": ^7.18.9 + "@babel/parser": ^7.18.13 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.18.13 + "@babel/types": ^7.18.13 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 @@ -137,7 +137,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:~7.18.13": +"@babel/core@npm:^7.18.13, @babel/core@npm:~7.18.13": version: 7.18.13 resolution: "@babel/core@npm:7.18.13" dependencies: @@ -174,11 +174,11 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.18.9, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.7.2": version: 7.18.9 resolution: "@babel/generator@npm:7.18.9" dependencies: - "@babel/types": ^7.18.9 + "@babel/types": ^7.18.13 "@jridgewell/gen-mapping": ^0.3.2 jsesc: ^2.5.1 checksum: 1c271e0c6f33e59f7845d88a1b0b9b0dce88164e80dec9274a716efa54c260e405e9462b160843e73f45382bf5b24d8e160e0121207e480c29b30e2ed0eb16d4 @@ -319,21 +319,19 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.3.1": - version: 0.3.1 - resolution: "@babel/helper-define-polyfill-provider@npm:0.3.1" +"@babel/helper-define-polyfill-provider@npm:^0.3.2": + version: 0.3.2 + resolution: "@babel/helper-define-polyfill-provider@npm:0.3.2" dependencies: - "@babel/helper-compilation-targets": ^7.13.0 - "@babel/helper-module-imports": ^7.12.13 - "@babel/helper-plugin-utils": ^7.13.0 - "@babel/traverse": ^7.13.0 + "@babel/helper-compilation-targets": ^7.17.7 + "@babel/helper-plugin-utils": ^7.16.7 debug: ^4.1.1 lodash.debounce: ^4.0.8 resolve: ^1.14.2 semver: ^6.1.2 peerDependencies: "@babel/core": ^7.4.0-0 - checksum: e3e93cb22febfc0449a210cdafb278e5e1a038af2ca2b02f5dee71c7a49e8ba26e469d631ee11a4243885961a62bb2e5b0a4deb3ec1d7918a33c953d05c3e584 + checksum: 8f693ab8e9d73873c2e547c7764c7d32d73c14f8dcefdd67fd3a038eb75527e2222aa53412ea673b9bfc01c32a8779a60e77a7381bbdd83452f05c9b7ef69c2c languageName: node linkType: hard @@ -478,21 +476,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-remap-async-to-generator@npm:7.18.6" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.6 - "@babel/helper-wrap-function": ^7.18.6 - "@babel/types": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 83e890624da9413c74a8084f6b5f7bfe93abad8a6e1a33464f3086e2a1336751672e6ac6d74dddd35b641d19584cc0f93d02c52a4f33385b3be5b40942fe30da - languageName: node - linkType: hard - -"@babel/helper-remap-async-to-generator@npm:^7.18.9": +"@babel/helper-remap-async-to-generator@npm:^7.18.6, @babel/helper-remap-async-to-generator@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" dependencies: @@ -567,18 +551,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-wrap-function@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-wrap-function@npm:7.18.6" - dependencies: - "@babel/helper-function-name": ^7.18.6 - "@babel/template": ^7.18.6 - "@babel/traverse": ^7.18.6 - "@babel/types": ^7.18.6 - checksum: b7a4f59b302ed77407e5c2005d8677ebdeabbfa69230e15f80b5e06cc532369c1e48399ec3e67dd3341e7ab9b3f84f17a255e2c1ec4e0d42bb571a4dac5472d6 - languageName: node - linkType: hard - "@babel/helper-wrap-function@npm:^7.18.9": version: 7.19.0 resolution: "@babel/helper-wrap-function@npm:7.19.0" @@ -624,7 +596,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.6, @babel/parser@npm:^7.18.9": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.6": version: 7.18.9 resolution: "@babel/parser@npm:7.18.9" bin: @@ -675,17 +647,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-async-generator-functions@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.18.6" +"@babel/plugin-proposal-async-generator-functions@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.18.10" dependencies: - "@babel/helper-environment-visitor": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/helper-remap-async-to-generator": ^7.18.6 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-plugin-utils": ^7.18.9 + "@babel/helper-remap-async-to-generator": ^7.18.9 "@babel/plugin-syntax-async-generators": ^7.8.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3f708808ba6f8a9bd18805b1b22ab90ec0b362d949111a776e0bade5391f143f55479dcc444b2cec25fc89ac21035ee92e9a5ec37c02c610639197a0c2f7dcb0 + checksum: 3a6c25085021053830f6c57780118d3337935ac3309eef7f09b11e413d189eed8119d50cbddeb4c8c02f42f8cc01e62a4667b869be6e158f40030bafb92a0629 languageName: node linkType: hard @@ -1706,17 +1678,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 297a03706723164a777263f76a8d89bccfb1d3fbc5e1075079dfd84372a5416d579da7d44c650abf935a1150a995bfce0e61966447b657f958e51c4ea45b72dc - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-unicode-regex@npm:7.18.6" @@ -1729,7 +1690,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.18.9": +"@babel/preset-env@npm:^7.12.11": version: 7.18.9 resolution: "@babel/preset-env@npm:7.18.9" dependencies: @@ -1739,7 +1700,7 @@ __metadata: "@babel/helper-validator-option": ^7.18.6 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-async-generator-functions": ^7.18.6 + "@babel/plugin-proposal-async-generator-functions": ^7.18.10 "@babel/plugin-proposal-class-properties": ^7.18.6 "@babel/plugin-proposal-class-static-block": ^7.18.6 "@babel/plugin-proposal-dynamic-import": ^7.18.6 @@ -1799,13 +1760,13 @@ __metadata: "@babel/plugin-transform-sticky-regex": ^7.18.6 "@babel/plugin-transform-template-literals": ^7.18.9 "@babel/plugin-transform-typeof-symbol": ^7.18.9 - "@babel/plugin-transform-unicode-escapes": ^7.18.6 + "@babel/plugin-transform-unicode-escapes": ^7.18.10 "@babel/plugin-transform-unicode-regex": ^7.18.6 "@babel/preset-modules": ^0.1.5 - "@babel/types": ^7.18.9 - babel-plugin-polyfill-corejs2: ^0.3.1 - babel-plugin-polyfill-corejs3: ^0.5.2 - babel-plugin-polyfill-regenerator: ^0.3.1 + "@babel/types": ^7.18.10 + babel-plugin-polyfill-corejs2: ^0.3.2 + babel-plugin-polyfill-corejs3: ^0.5.3 + babel-plugin-polyfill-regenerator: ^0.4.0 core-js-compat: ^3.22.1 semver: ^6.3.0 peerDependencies: @@ -1814,6 +1775,91 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.18.10": + version: 7.18.10 + resolution: "@babel/preset-env@npm:7.18.10" + dependencies: + "@babel/compat-data": ^7.18.8 + "@babel/helper-compilation-targets": ^7.18.9 + "@babel/helper-plugin-utils": ^7.18.9 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-async-generator-functions": ^7.18.10 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-class-static-block": ^7.18.6 + "@babel/plugin-proposal-dynamic-import": ^7.18.6 + "@babel/plugin-proposal-export-namespace-from": ^7.18.9 + "@babel/plugin-proposal-json-strings": ^7.18.6 + "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 + "@babel/plugin-proposal-numeric-separator": ^7.18.6 + "@babel/plugin-proposal-object-rest-spread": ^7.18.9 + "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 + "@babel/plugin-proposal-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-private-methods": ^7.18.6 + "@babel/plugin-proposal-private-property-in-object": ^7.18.6 + "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.18.6 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.18.6 + "@babel/plugin-transform-async-to-generator": ^7.18.6 + "@babel/plugin-transform-block-scoped-functions": ^7.18.6 + "@babel/plugin-transform-block-scoping": ^7.18.9 + "@babel/plugin-transform-classes": ^7.18.9 + "@babel/plugin-transform-computed-properties": ^7.18.9 + "@babel/plugin-transform-destructuring": ^7.18.9 + "@babel/plugin-transform-dotall-regex": ^7.18.6 + "@babel/plugin-transform-duplicate-keys": ^7.18.9 + "@babel/plugin-transform-exponentiation-operator": ^7.18.6 + "@babel/plugin-transform-for-of": ^7.18.8 + "@babel/plugin-transform-function-name": ^7.18.9 + "@babel/plugin-transform-literals": ^7.18.9 + "@babel/plugin-transform-member-expression-literals": ^7.18.6 + "@babel/plugin-transform-modules-amd": ^7.18.6 + "@babel/plugin-transform-modules-commonjs": ^7.18.6 + "@babel/plugin-transform-modules-systemjs": ^7.18.9 + "@babel/plugin-transform-modules-umd": ^7.18.6 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.18.6 + "@babel/plugin-transform-new-target": ^7.18.6 + "@babel/plugin-transform-object-super": ^7.18.6 + "@babel/plugin-transform-parameters": ^7.18.8 + "@babel/plugin-transform-property-literals": ^7.18.6 + "@babel/plugin-transform-regenerator": ^7.18.6 + "@babel/plugin-transform-reserved-words": ^7.18.6 + "@babel/plugin-transform-shorthand-properties": ^7.18.6 + "@babel/plugin-transform-spread": ^7.18.9 + "@babel/plugin-transform-sticky-regex": ^7.18.6 + "@babel/plugin-transform-template-literals": ^7.18.9 + "@babel/plugin-transform-typeof-symbol": ^7.18.9 + "@babel/plugin-transform-unicode-escapes": ^7.18.10 + "@babel/plugin-transform-unicode-regex": ^7.18.6 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.18.10 + babel-plugin-polyfill-corejs2: ^0.3.2 + babel-plugin-polyfill-corejs3: ^0.5.3 + babel-plugin-polyfill-regenerator: ^0.4.0 + core-js-compat: ^3.22.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 36eeb7157021091c8047703833b7a28e4963865d16968a5b9dbffe1eb05e44307a8d29ad45d81fd23817f68290b52921c42f513a93996c7083d23d5e2cea0c6b + languageName: node + linkType: hard + "@babel/preset-env@npm:^7.19.1": version: 7.19.1 resolution: "@babel/preset-env@npm:7.19.1" @@ -1993,16 +2039,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.18.6 - resolution: "@babel/runtime@npm:7.18.6" - dependencies: - regenerator-runtime: ^0.13.4 - checksum: 8b707b64ae0524db617d0c49933b258b96376a38307dc0be8fb42db5697608bcc1eba459acce541e376cff5ed5c5287d24db5780bd776b7c75ba2c2e26ff8a2c - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.18.9": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.18.9 resolution: "@babel/runtime@npm:7.18.9" dependencies: @@ -2051,18 +2088,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.18.6, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.7.2": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.7.2": version: 7.18.9 resolution: "@babel/traverse@npm:7.18.9" dependencies: "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.18.9 + "@babel/generator": ^7.18.13 "@babel/helper-environment-visitor": ^7.18.9 "@babel/helper-function-name": ^7.18.9 "@babel/helper-hoist-variables": ^7.18.6 "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.18.9 - "@babel/types": ^7.18.9 + "@babel/parser": ^7.18.13 + "@babel/types": ^7.18.13 debug: ^4.1.0 globals: ^11.1.0 checksum: 0445a51952ea1664a5719d9b1f8bf04be6f1933bcf54915fecc544c844a5dad2ac56f3b555723bbf741ef680d7fd64f6a5d69cfd08d518a4089c79a734270162 @@ -2109,6 +2146,7 @@ __metadata: version: 7.18.9 resolution: "@babel/types@npm:7.18.9" dependencies: + "@babel/helper-string-parser": ^7.18.10 "@babel/helper-validator-identifier": ^7.18.6 to-fast-properties: ^2.0.0 checksum: f0e0147267895fd8a5b82133e711ce7ce99941f3ce63647e0e3b00656a7afe48a8aa48edbae27543b701794d2b29a562a08f51f88f41df401abce7c3acc5e13a @@ -2457,7 +2495,7 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.0.5, @eslint/eslintrc@npm:^1.3.0": +"@eslint/eslintrc@npm:^1.3.0": version: 1.3.0 resolution: "@eslint/eslintrc@npm:1.3.0" dependencies: @@ -2631,17 +2669,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.9.2": - version: 0.9.5 - resolution: "@humanwhocodes/config-array@npm:0.9.5" - dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 - minimatch: ^3.0.4 - checksum: 8ba6281bc0590f6c6eadeefc14244b5a3e3f5903445aadd1a32099ed80e753037674026ce1b3c945ab93561bea5eb29e3c5bff67060e230c295595ba517a3492 - languageName: node - linkType: hard - "@humanwhocodes/gitignore-to-minimatch@npm:^1.0.2": version: 1.0.2 resolution: "@humanwhocodes/gitignore-to-minimatch@npm:1.0.2" @@ -3249,7 +3276,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.0, @jridgewell/trace-mapping@npm:^0.3.7, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.0, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.13 resolution: "@jridgewell/trace-mapping@npm:0.3.13" dependencies: @@ -5444,7 +5471,7 @@ __metadata: cron: ~1.8.0 date.js: ~0.3.3 debug: ~4.1.1 - eslint: ^8.20.0 + eslint: ^8.22.0 human-interval: ^2.0.0 jest: ^27.5.1 moment-timezone: ~0.5.27 @@ -5462,7 +5489,7 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/strict-uri-encode": ^2.0.0 - eslint: ^8.20.0 + eslint: ^8.22.0 filter-obj: ^3.0.0 jest: ^27.5.1 query-string: ^7.1.1 @@ -5521,7 +5548,7 @@ __metadata: dependencies: "@types/jest": ^27.4.1 cheerio: 1.0.0-rc.10 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -5537,7 +5564,7 @@ __metadata: "@rocket.chat/icons": next "@rocket.chat/message-parser": next "@rocket.chat/ui-kit": next - eslint: ^8.20.0 + eslint: ^8.22.0 mongodb: ^4.3.1 prettier: ^2.7.1 typescript: ~4.5.5 @@ -5588,7 +5615,7 @@ __metadata: "@types/ws": ^8.5.3 colorette: ^1.4.0 ejson: ^2.2.2 - eslint: ^8.21.0 + eslint: ^8.22.0 eventemitter3: ^4.0.7 fibers: ^5.0.1 jaeger-client: ^3.19.0 @@ -5633,7 +5660,7 @@ __metadata: "@types/prettier": ^2.6.3 "@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/parser": ^5.30.7 - eslint: ^8.20.0 + eslint: ^8.22.0 eslint-config-prettier: ^8.5.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-import: ^2.26.0 @@ -5647,7 +5674,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/favicon@workspace:packages/favicon" dependencies: - eslint: ^8.20.0 + eslint: ^8.22.0 typescript: ~4.5.5 languageName: unknown linkType: soft @@ -5759,25 +5786,7 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-ui-kit@npm:next": - version: 0.32.0-dev.101 - resolution: "@rocket.chat/fuselage-ui-kit@npm:0.32.0-dev.101" - dependencies: - "@rocket.chat/ui-kit": ~0.32.0-dev.86 - tslib: ^2.3.1 - peerDependencies: - "@rocket.chat/fuselage": "*" - "@rocket.chat/fuselage-hooks": "*" - "@rocket.chat/fuselage-polyfills": "*" - "@rocket.chat/icons": "*" - "@rocket.chat/styled": "*" - react: ^17.0.2 - react-dom: ^17.0.2 - checksum: 806b0192e1c56c4309ebe5d00f710f09a52f3c8e110025741d684057afa46ca104872e7c8c58af5a543fc992537f9ff6fd2438982de40edcef96a4a065abab44 - languageName: node - linkType: hard - -"@rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit": +"@rocket.chat/fuselage-ui-kit@workspace:^, @rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit": version: 0.0.0-use.local resolution: "@rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit" dependencies: @@ -5789,7 +5798,9 @@ __metadata: "@rocket.chat/icons": next "@rocket.chat/prettier-config": next "@rocket.chat/styled": next + "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": next + "@rocket.chat/ui-video-conf": "workspace:^" "@storybook/addon-essentials": ~6.5.10 "@storybook/addons": ~6.5.10 "@storybook/builder-webpack5": ~6.5.10 @@ -5797,29 +5808,36 @@ __metadata: "@storybook/react": ~6.5.10 "@storybook/source-loader": ~6.5.10 "@storybook/theming": ~6.5.10 - "@types/react": ~17.0.39 - "@types/react-dom": ^17.0.11 + "@tanstack/react-query": ^4.2.1 + "@types/react": ~17.0.48 + "@types/react-dom": ~17.0.17 babel-loader: ~8.2.3 cross-env: ^7.0.3 - eslint: ~8.8.0 + eslint: ~8.22.0 lint-staged: ~12.3.3 normalize.css: ^8.0.1 npm-run-all: ^4.1.5 prettier: ~2.5.1 - react: ^17.0.2 react-dom: ^17.0.2 rimraf: ^3.0.2 tslib: ^2.3.1 typescript: ~4.3.5 webpack: ~5.68.0 peerDependencies: + "@rocket.chat/apps-engine": "*" + "@rocket.chat/eslint-config": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/fuselage-polyfills": "*" "@rocket.chat/icons": "*" + "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - react: ^17.0.2 - react-dom: ^17.0.2 + "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-kit": "*" + "@rocket.chat/ui-video-conf": "*" + "@tanstack/react-query": "*" + react: "*" + react-dom: "*" languageName: unknown linkType: soft @@ -5849,11 +5867,10 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/gazzodown@workspace:packages/gazzodown" dependencies: - "@babel/core": ^7.18.9 + "@babel/core": ~7.18.13 "@mdx-js/react": ^1.6.22 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": next "@rocket.chat/fuselage-tokens": next "@rocket.chat/message-parser": next "@rocket.chat/styled": next @@ -5873,13 +5890,13 @@ __metadata: "@types/babel__core": ^7 "@types/jest": ^27.4.1 "@types/katex": ~0 - "@types/react": ^17.0.47 - "@types/react-dom": ^18 + "@types/react": ~17.0.48 + "@types/react-dom": ~17.0.17 "@types/testing-library__jest-dom": ^5 "@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/parser": ^5.30.7 babel-loader: ^8.2.5 - eslint: ^8.20.0 + eslint: ^8.22.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -5893,16 +5910,16 @@ __metadata: ts-jest: ^27.1.4 typescript: ~4.5.5 peerDependencies: - "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/core-typings": "*" "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": "workspace:^" - "@rocket.chat/ui-contexts": "workspace:^" + "@rocket.chat/ui-client": "*" + "@rocket.chat/ui-contexts": "*" katex: "*" - react: ~17.0.2 + react: "*" languageName: unknown linkType: soft @@ -5930,7 +5947,7 @@ __metadata: resolution: "@rocket.chat/livechat@workspace:packages/livechat" dependencies: "@babel/eslint-parser": ^7.18.9 - "@babel/preset-env": ^7.18.9 + "@babel/preset-env": ^7.18.10 "@rocket.chat/eslint-config": ^0.4.0 "@rocket.chat/fuselage-tokens": next "@rocket.chat/logo": next @@ -5944,7 +5961,6 @@ __metadata: "@storybook/addon-viewport": ~6.5.10 "@storybook/react": ~6.5.10 "@storybook/theming": ~6.5.10 - "@types/react-dom": ^17.0.17 autoprefixer: ^9.8.8 babel-loader: ^8.1.0 babel-plugin-jsx-pragmatic: ^1.0.2 @@ -5956,7 +5972,7 @@ __metadata: date-fns: ^2.15.0 desvg-loader: ^0.1.0 emoji-mart: ^3.0.1 - eslint: ^8.20.0 + eslint: ^8.22.0 eslint-plugin-import: ^2.26.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -6040,11 +6056,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/meteor@workspace:apps/meteor" dependencies: - "@babel/core": ^7.18.9 + "@babel/core": ^7.18.13 "@babel/eslint-parser": ^7.18.9 "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/preset-env": ^7.18.9 + "@babel/preset-env": ^7.18.10 "@babel/preset-react": ^7.18.6 "@babel/register": ^7.18.9 "@babel/runtime": ^7.18.9 @@ -6073,7 +6089,7 @@ __metadata: "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-toastbar": next "@rocket.chat/fuselage-tokens": next - "@rocket.chat/fuselage-ui-kit": next + "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/icons": next "@rocket.chat/layout": next @@ -6156,7 +6172,7 @@ __metadata: "@types/proxy-from-env": ^1.0.1 "@types/proxyquire": ^1.3.28 "@types/psl": ^1.1.0 - "@types/react": ~17.0.47 + "@types/react": ~17.0.48 "@types/react-dom": ~17.0.17 "@types/rewire": ^2.5.28 "@types/semver": ^7.3.10 @@ -6219,7 +6235,7 @@ __metadata: emailreplyparser: ^0.0.5 emojione: ^4.5.0 emojione-assets: ^4.5.0 - eslint: ^8.20.0 + eslint: ^8.22.0 eslint-config-prettier: ^8.5.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-import: ^2.26.0 @@ -6370,7 +6386,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/node-rsa": ^1.1.1 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 mongodb: ^4.3.1 ts-jest: ^27.1.5 @@ -6384,7 +6400,7 @@ __metadata: dependencies: "@rocket.chat/model-typings": "workspace:^" "@types/jest": ^27.4.1 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 ts-jest: ^27.1.5 typescript: ~4.5.5 @@ -6427,7 +6443,7 @@ __metadata: resolution: "@rocket.chat/poplib@workspace:packages/node-poplib" dependencies: "@types/jest": ^27.4.1 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -6501,7 +6517,7 @@ __metadata: "@rocket.chat/ui-kit": next "@types/jest": ^27.4.1 ajv: ^8.11.0 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 mongodb: ^4.3.1 ts-jest: ^27.1.5 @@ -6557,6 +6573,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" dependencies: + "@babel/core": ~7.18.13 "@rocket.chat/css-in-js": next "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": next @@ -6572,10 +6589,12 @@ __metadata: "@storybook/manager-webpack4": ~6.5.10 "@storybook/react": ~6.5.10 "@storybook/testing-library": ~0.0.13 + "@types/babel__core": ^7 "@types/jest": ^27.4.1 "@types/postcss-url": ^10 - "@types/react": ~17.0.47 - eslint: ^8.20.0 + "@types/react": ~17.0.48 + "@types/react-dom": ~17.0.17 + eslint: ^8.22.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -6596,6 +6615,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" + "@rocket.chat/ui-contexts": "*" react: ~17.0.2 languageName: unknown linkType: soft @@ -6641,9 +6661,10 @@ __metadata: "@rocket.chat/fuselage-hooks": next "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 - "@types/react": ^17.0.47 + "@types/react": ~17.0.48 + "@types/react-dom": ~17.0.17 "@types/use-sync-external-store": ^0.0.3 - eslint: ^8.20.0 + eslint: ^8.22.0 jest: ^27.5.1 mongodb: ^4.3.1 react: ~17.0.2 @@ -6660,7 +6681,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/ui-kit@npm:next, @rocket.chat/ui-kit@npm:~0.32.0-dev.86": +"@rocket.chat/ui-kit@npm:next": version: 0.32.0-dev.86 resolution: "@rocket.chat/ui-kit@npm:0.32.0-dev.86" checksum: 234a8a384cdfd881074bfd357de7f3805a6af1798cc73d66fd89735abad7d6d0414f1bbe674cc805c0b8c6cf897364b257027c383cc3e8be4bb26207485332a7 @@ -6671,15 +6692,27 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-video-conf@workspace:packages/ui-video-conf" dependencies: + "@babel/core": ~7.18.13 "@rocket.chat/css-in-js": next + "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": next + "@rocket.chat/icons": next "@rocket.chat/styled": next + "@storybook/addon-actions": ~6.5.10 + "@storybook/addon-docs": ~6.5.10 + "@storybook/addon-essentials": ~6.5.10 + "@storybook/builder-webpack4": ~6.5.10 + "@storybook/manager-webpack4": ~6.5.10 + "@storybook/react": ~6.5.10 + "@storybook/testing-library": ~0.0.13 + "@types/babel__core": ^7 "@types/jest": ^27.4.1 - eslint: ^8.20.0 + eslint: ^8.22.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-storybook: ^0.6.4 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -6687,6 +6720,7 @@ __metadata: "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" + "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 @@ -7582,28 +7616,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/addons@npm:6.5.9" - dependencies: - "@storybook/api": 6.5.9 - "@storybook/channels": 6.5.9 - "@storybook/client-logger": 6.5.9 - "@storybook/core-events": 6.5.9 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.9 - "@storybook/theming": 6.5.9 - "@types/webpack-env": ^1.16.0 - core-js: ^3.8.2 - global: ^4.4.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 50e0579df27aa7d405e25c0f057e4cd2d37c091ee4b88ab7969238255738ab5eb7f8c5af3100eaeaea74f916288ed862291f517b8a05e30578d7d1fd254d9f8c - languageName: node - linkType: hard - "@storybook/api@npm:6.5.10": version: 6.5.10 resolution: "@storybook/api@npm:6.5.10" @@ -7660,34 +7672,6 @@ __metadata: languageName: node linkType: hard -"@storybook/api@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/api@npm:6.5.9" - dependencies: - "@storybook/channels": 6.5.9 - "@storybook/client-logger": 6.5.9 - "@storybook/core-events": 6.5.9 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.9 - "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.9 - core-js: ^3.8.2 - fast-deep-equal: ^3.1.3 - global: ^4.4.0 - lodash: ^4.17.21 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - store2: ^2.12.0 - telejson: ^6.0.8 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 72d720eba7a5f6645c92a18884e267b57d4ba145d9aafd891f3a9c7651e8ea1418ada7cf7f6d5d963db100526103d5fceac8fb0a82e8099478b02dc8f33a1fd7 - languageName: node - linkType: hard - "@storybook/builder-webpack4@npm:6.5.10, @storybook/builder-webpack4@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/builder-webpack4@npm:6.5.10" @@ -7940,17 +7924,6 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/channels@npm:6.5.9" - dependencies: - core-js: ^3.8.2 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - checksum: b51767553a3e00f4da8e9684c798348c230d5553a43886ca560c7e2f249e15ab9e3d7bbeb947d394413505261806c79c629551f9d722f83f00e15d9e19b6617c - languageName: node - linkType: hard - "@storybook/client-api@npm:6.5.10": version: 6.5.10 resolution: "@storybook/client-api@npm:6.5.10" @@ -8033,7 +8006,7 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.9, @storybook/client-logger@npm:^6.4.0": +"@storybook/client-logger@npm:^6.4.0": version: 6.5.9 resolution: "@storybook/client-logger@npm:6.5.9" dependencies: @@ -8297,15 +8270,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/core-events@npm:6.5.9" - dependencies: - core-js: ^3.8.2 - checksum: b28af71de1e7f66a6fdf26c384c976640220ea1a6d807523ec368ecdc1b9dd3c87d5e1fcc5bd443d1059c408c17288afb415f8160e69ebb6cb2f3914a2db5f1d - languageName: node - linkType: hard - "@storybook/core-server@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core-server@npm:6.5.10" @@ -8576,7 +8540,7 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:6.5.10": +"@storybook/instrumenter@npm:6.5.10, @storybook/instrumenter@npm:^6.4.0": version: 6.5.10 resolution: "@storybook/instrumenter@npm:6.5.10" dependencies: @@ -8589,19 +8553,6 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:^6.4.0": - version: 6.5.9 - resolution: "@storybook/instrumenter@npm:6.5.9" - dependencies: - "@storybook/addons": 6.5.9 - "@storybook/client-logger": 6.5.9 - "@storybook/core-events": 6.5.9 - core-js: ^3.8.2 - global: ^4.4.0 - checksum: 90b83a30177794cbbd9e388b4ee68ee6c86c28d5bbb4c992607f3e5ed189abdb8191cfa392d912a45b6a42e5eb603e90a853032205a4b9a4388f3af8f49d5064 - languageName: node - linkType: hard - "@storybook/manager-webpack4@npm:6.5.10, @storybook/manager-webpack4@npm:~6.5.10": version: 6.5.10 resolution: "@storybook/manager-webpack4@npm:6.5.10" @@ -9056,22 +9007,6 @@ __metadata: languageName: node linkType: hard -"@storybook/router@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/router@npm:6.5.9" - dependencies: - "@storybook/client-logger": 6.5.9 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - qs: ^6.10.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10acf6d67fa245ca10d8e377d593405ab1505d22b3bb2e7ce7dc45bc5be2074d7bb89f9266b7550b84063c907e2188742b355fc8af05f7cf4554a0770915d12e - languageName: node - linkType: hard - "@storybook/semver@npm:^7.3.2": version: 7.3.2 resolution: "@storybook/semver@npm:7.3.2" @@ -9261,21 +9196,6 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:6.5.9": - version: 6.5.9 - resolution: "@storybook/theming@npm:6.5.9" - dependencies: - "@storybook/client-logger": 6.5.9 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 0c0d034864bcf7289778aa549dd9d830c75b90e416cbd2ee8bc9be946f1699141a7b695916aa134c38d156edcfac3a1378e3490ac02b470b89d168625618d073 - languageName: node - linkType: hard - "@storybook/ui@npm:6.5.10": version: 6.5.10 resolution: "@storybook/ui@npm:6.5.10" @@ -9333,9 +9253,9 @@ __metadata: languageName: node linkType: hard -"@tanstack/react-query@npm:^4.0.10": - version: 4.0.10 - resolution: "@tanstack/react-query@npm:4.0.10" +"@tanstack/react-query@npm:^4.0.10, @tanstack/react-query@npm:^4.2.1": + version: 4.2.1 + resolution: "@tanstack/react-query@npm:4.2.1" dependencies: "@tanstack/query-core": ^4.0.0-beta.1 "@types/use-sync-external-store": ^0.0.3 @@ -9349,7 +9269,7 @@ __metadata: optional: true react-native: optional: true - checksum: b5f7501f977f4262fe80e510f5d58f3b49163ff7232603a839b7184431db7309c38ab55c5856a9d31a3c5219dd5031f9174e9dfcc78408300d0424b3000b9034 + checksum: bbf3a808645c26c649971dc182bb9a7ed7a1d89f6456b60685c6081b8be6ae84ae83b39c7eacb96c4f3b6677ca001d8114037329951987b7a8d65de53b28c862 languageName: node linkType: hard @@ -9724,7 +9644,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.0": +"@types/eslint-scope@npm:^3.7.0, @types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" dependencies: @@ -9734,16 +9654,6 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.3": - version: 3.7.3 - resolution: "@types/eslint-scope@npm:3.7.3" - dependencies: - "@types/eslint": "*" - "@types/estree": "*" - checksum: 6772b05e1b92003d1f295e81bc847a61f4fbe8ddab77ffa49e84ed3f9552513bdde677eb53ef167753901282857dd1d604d9f82eddb34a233495932b2dc3dc17 - languageName: node - linkType: hard - "@types/eslint@npm:*, @types/eslint@npm:^8": version: 8.4.5 resolution: "@types/eslint@npm:8.4.5" @@ -10375,7 +10285,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:<18.0.0, @types/react-dom@npm:^17.0.11, @types/react-dom@npm:^17.0.17, @types/react-dom@npm:~17.0.17": +"@types/react-dom@npm:<18.0.0, @types/react-dom@npm:~17.0.17": version: 17.0.17 resolution: "@types/react-dom@npm:17.0.17" dependencies: @@ -10384,27 +10294,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18": - version: 18.0.5 - resolution: "@types/react-dom@npm:18.0.5" - dependencies: - "@types/react": "*" - checksum: cd48b81950f499b52a3f0c08261f00046f9b7c96699fa249c9664e257e820daf6ecac815cd1028cebc9d105094adc39d047d1efd79214394b8b2d515574c0787 - languageName: node - linkType: hard - -"@types/react@npm:*, @types/react@npm:^17, @types/react@npm:^17.0.47, @types/react@npm:~17.0.47": - version: 17.0.47 - resolution: "@types/react@npm:17.0.47" - dependencies: - "@types/prop-types": "*" - "@types/scheduler": "*" - csstype: ^3.0.2 - checksum: 2e7fe0eb630cb77da03b6da308c58728c01b38e878118e9ff5cd8045181c8d4f32dc936e328f46a62cadb56e1fe4c5a911b5113584f93a99e1f35df7f059246b - languageName: node - linkType: hard - -"@types/react@npm:~17.0.39": +"@types/react@npm:*, @types/react@npm:^17, @types/react@npm:~17.0.48": version: 17.0.48 resolution: "@types/react@npm:17.0.48" dependencies: @@ -10846,6 +10736,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.33.0": + version: 5.33.0 + resolution: "@typescript-eslint/scope-manager@npm:5.33.0" + dependencies: + "@typescript-eslint/types": 5.33.0 + "@typescript-eslint/visitor-keys": 5.33.0 + checksum: b2cbea9abd528d01a5acb2d68a2a5be51ec6827760d3869bdd70920cf6c3a4f9f96d87c77177f8313009d9db71253e4a75f8393f38651e2abaf91ef28e60fb9d + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/scope-manager@npm:5.36.2" @@ -10879,6 +10779,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.33.0": + version: 5.33.0 + resolution: "@typescript-eslint/types@npm:5.33.0" + checksum: 8bbddda84cb3adf5c659b0d42547a2d6ab87f4eea574aca5dd63a3bd85169f32796ecbddad3b27f18a609070f6b1d18a54018d488bad746ae0f6ea5c02206109 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/types@npm:5.36.2" @@ -10904,6 +10811,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.33.0": + version: 5.33.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.33.0" + dependencies: + "@typescript-eslint/types": 5.33.0 + "@typescript-eslint/visitor-keys": 5.33.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 26f9005cdfb14654125a33d90d872b926820e560dff8970c4629fd5f6f47ad2a31e4c63161564d21bb42a8fc3ced0033994854ee37336ae07d90ccf6300d702b + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/typescript-estree@npm:5.36.2" @@ -10922,7 +10847,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.30.7, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": +"@typescript-eslint/utils@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/utils@npm:5.30.7" dependencies: @@ -10954,6 +10879,22 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": + version: 5.33.0 + resolution: "@typescript-eslint/utils@npm:5.33.0" + dependencies: + "@types/json-schema": ^7.0.9 + "@typescript-eslint/scope-manager": 5.33.0 + "@typescript-eslint/types": 5.33.0 + "@typescript-eslint/typescript-estree": 5.33.0 + eslint-scope: ^5.1.1 + eslint-utils: ^3.0.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 6ce5ee5eabeb6d73538b24e6487f811ecb0ef3467bd366cbd15bf30d904bdedb73fc6f48cf2e2e742dda462b42999ea505e8b59255545825ec9db86f3d423ea7 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/visitor-keys@npm:5.30.7" @@ -10964,6 +10905,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.33.0": + version: 5.33.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.33.0" + dependencies: + "@typescript-eslint/types": 5.33.0 + eslint-visitor-keys: ^3.3.0 + checksum: d7e3653de6bac6841e6fcc54226b93ad6bdca4aa76ebe7d83459c016c3eebcc50d4f65ee713174bc267d765295b642d1927a778c5de707b8389e3fcc052aa4a1 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/visitor-keys@npm:5.36.2" @@ -12667,16 +12618,16 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.3.1": - version: 0.3.1 - resolution: "babel-plugin-polyfill-corejs2@npm:0.3.1" +"babel-plugin-polyfill-corejs2@npm:^0.3.2": + version: 0.3.2 + resolution: "babel-plugin-polyfill-corejs2@npm:0.3.2" dependencies: - "@babel/compat-data": ^7.13.11 - "@babel/helper-define-polyfill-provider": ^0.3.1 + "@babel/compat-data": ^7.17.7 + "@babel/helper-define-polyfill-provider": ^0.3.2 semver: ^6.1.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: ca873f14ccd6d2942013345a956de8165d0913556ec29756a748157140f5312f79eed487674e0ca562285880f05829b3712d72e1e4b412c2ce46bd6a50b4b975 + checksum: a76e7bb1a5cc0a4507baa523c23f9efd75764069a25845beba92290386e5e48ed85b894005ece3b527e13c3d2d9c6589cc0a23befb72ea6fc7aa8711f231bb4d languageName: node linkType: hard @@ -12705,15 +12656,15 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.5.2": - version: 0.5.2 - resolution: "babel-plugin-polyfill-corejs3@npm:0.5.2" +"babel-plugin-polyfill-corejs3@npm:^0.5.3": + version: 0.5.3 + resolution: "babel-plugin-polyfill-corejs3@npm:0.5.3" dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.1 + "@babel/helper-define-polyfill-provider": ^0.3.2 core-js-compat: ^3.21.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 2f3184c73f80f00ac876a5ebcad945fd8d2ae70e5f85b7ab6cc3bc69bc74025f4f7070de7abbb2a7274c78e130bd34fc13f4c85342da28205930364a1ef0aa21 + checksum: 9c6644a1b0afbe59e402827fdafc6f44994ff92c5b2f258659cbbfd228f7075dea49e95114af10e66d70f36cbde12ff1d81263eb67be749b3ef0e2c18cf3c16d languageName: node linkType: hard @@ -12729,14 +12680,14 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.3.1": - version: 0.3.1 - resolution: "babel-plugin-polyfill-regenerator@npm:0.3.1" +"babel-plugin-polyfill-regenerator@npm:^0.4.0": + version: 0.4.0 + resolution: "babel-plugin-polyfill-regenerator@npm:0.4.0" dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.1 + "@babel/helper-define-polyfill-provider": ^0.3.2 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: f1473df7b700d6795ca41301b1e65a0aff15ce6c1463fc0ce2cf0c821114b0330920f59d4cebf52976363ee817ba29ad2758544a4661a724b08191080b9fe1da + checksum: 699aa9c0dc5a2259d7fa52b26613fa1e782439eee54cd98506991f87fddf0c00eec6c5b1917edf586c170731d9e318903bc41210225a691e7bb8087652bbda94 languageName: node linkType: hard @@ -13957,9 +13908,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001349": - version: 1.0.30001378 - resolution: "caniuse-lite@npm:1.0.30001378" - checksum: 19f1774da1f62d393ddde55dc091eb3e4f5c5b0ce43f9a9d20e75307a0f329cf8591c836a35a9f6f9fd7c27db7a75e0682245a194acec2e2ba1bc25ef1c3300c + version: 1.0.30001352 + resolution: "caniuse-lite@npm:1.0.30001352" + checksum: 575ad031349e56224471859decd100d0f90c804325bf1b543789b212d6126f6e18925766b325b1d96f75e48df0036e68f92af26d1fb175803fd6ad935bc807ac languageName: node linkType: hard @@ -17180,16 +17131,6 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.9.3": - version: 5.9.3 - resolution: "enhanced-resolve@npm:5.9.3" - dependencies: - graceful-fs: ^4.2.4 - tapable: ^2.2.0 - checksum: 64c2dbbdd608d1a4df93b6e60786c603a1faf3b2e66dfd051d62cf4cfaeeb5e800166183685587208d62e9f7afff3f78f3d5978e32cd80125ba0c83b59a79d78 - languageName: node - linkType: hard - "enquirer@npm:2.3.6, enquirer@npm:^2.3.5": version: 2.3.6 resolution: "enquirer@npm:2.3.6" @@ -17613,18 +17554,7 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-testing-library@npm:^5.5.1": - version: 5.5.1 - resolution: "eslint-plugin-testing-library@npm:5.5.1" - dependencies: - "@typescript-eslint/utils": ^5.13.0 - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - checksum: 558994da12e6a9ff0c4f71c2e63a23746b6323d171062032843591e0fca6ce3811f979cf82e11db003c8b4f1d9842cb75301bfaa9e88d1a399b11ea6686aadcc - languageName: node - linkType: hard - -"eslint-plugin-testing-library@npm:^5.6.0": +"eslint-plugin-testing-library@npm:^5.5.1, eslint-plugin-testing-library@npm:^5.6.0": version: 5.6.0 resolution: "eslint-plugin-testing-library@npm:5.6.0" dependencies: @@ -17655,7 +17585,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.1.0, eslint-scope@npm:^7.1.1": +"eslint-scope@npm:^7.1.1": version: 7.1.1 resolution: "eslint-scope@npm:7.1.1" dependencies: @@ -17699,7 +17629,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.2.0, eslint-visitor-keys@npm:^3.3.0": +"eslint-visitor-keys@npm:^3.3.0": version: 3.3.0 resolution: "eslint-visitor-keys@npm:3.3.0" checksum: d59e68a7c5a6d0146526b0eec16ce87fbf97fe46b8281e0d41384224375c4e52f5ffb9e16d48f4ea50785cde93f766b0c898e31ab89978d88b0e1720fbfb7808 @@ -17756,13 +17686,14 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.20.0, eslint@npm:^8.21.0": - version: 8.21.0 - resolution: "eslint@npm:8.21.0" +"eslint@npm:^8.21.0": + version: 8.23.1 + resolution: "eslint@npm:8.23.1" dependencies: - "@eslint/eslintrc": ^1.3.0 + "@eslint/eslintrc": ^1.3.2 "@humanwhocodes/config-array": ^0.10.4 "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 + "@humanwhocodes/module-importer": ^1.0.1 ajv: ^6.10.0 chalk: ^4.0.0 cross-spawn: ^7.0.2 @@ -17772,13 +17703,12 @@ __metadata: eslint-scope: ^7.1.1 eslint-utils: ^3.0.0 eslint-visitor-keys: ^3.3.0 - espree: ^9.3.3 + espree: ^9.4.0 esquery: ^1.4.0 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 file-entry-cache: ^6.0.1 find-up: ^5.0.0 - functional-red-black-tree: ^1.0.1 glob-parent: ^6.0.1 globals: ^13.15.0 globby: ^11.1.0 @@ -17787,6 +17717,7 @@ __metadata: import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 + js-sdsl: ^4.1.4 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 @@ -17798,21 +17729,19 @@ __metadata: strip-ansi: ^6.0.1 strip-json-comments: ^3.1.0 text-table: ^0.2.0 - v8-compile-cache: ^2.0.3 bin: eslint: bin/eslint.js - checksum: 1d39ddb08772ea230cb7d74f7f81f85b9d46965d3600725c7eb39a27bcdaf28cb2a780dacf6cfa1cfbf2da606b57a5e7e3ab373ab474cbcf0ba042076821f501 + checksum: a727e15492786a03b438bcf021db49f715680679846a7b8d79b98ad34576f2a570404ffe882d3c3e26f6359bff7277ef11fae5614bfe8629adb653f20d018c71 languageName: node linkType: hard -"eslint@npm:^8.22.0": - version: 8.23.1 - resolution: "eslint@npm:8.23.1" +"eslint@npm:^8.22.0, eslint@npm:~8.22.0": + version: 8.22.0 + resolution: "eslint@npm:8.22.0" dependencies: - "@eslint/eslintrc": ^1.3.2 + "@eslint/eslintrc": ^1.3.0 "@humanwhocodes/config-array": ^0.10.4 "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 - "@humanwhocodes/module-importer": ^1.0.1 ajv: ^6.10.0 chalk: ^4.0.0 cross-spawn: ^7.0.2 @@ -17822,12 +17751,13 @@ __metadata: eslint-scope: ^7.1.1 eslint-utils: ^3.0.0 eslint-visitor-keys: ^3.3.0 - espree: ^9.4.0 + espree: ^9.3.3 esquery: ^1.4.0 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 file-entry-cache: ^6.0.1 find-up: ^5.0.0 + functional-red-black-tree: ^1.0.1 glob-parent: ^6.0.1 globals: ^13.15.0 globby: ^11.1.0 @@ -17836,7 +17766,6 @@ __metadata: import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 - js-sdsl: ^4.1.4 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 @@ -17848,54 +17777,10 @@ __metadata: strip-ansi: ^6.0.1 strip-json-comments: ^3.1.0 text-table: ^0.2.0 - bin: - eslint: bin/eslint.js - checksum: a727e15492786a03b438bcf021db49f715680679846a7b8d79b98ad34576f2a570404ffe882d3c3e26f6359bff7277ef11fae5614bfe8629adb653f20d018c71 - languageName: node - linkType: hard - -"eslint@npm:~8.8.0": - version: 8.8.0 - resolution: "eslint@npm:8.8.0" - dependencies: - "@eslint/eslintrc": ^1.0.5 - "@humanwhocodes/config-array": ^0.9.2 - ajv: ^6.10.0 - chalk: ^4.0.0 - cross-spawn: ^7.0.2 - debug: ^4.3.2 - doctrine: ^3.0.0 - escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.0 - eslint-utils: ^3.0.0 - eslint-visitor-keys: ^3.2.0 - espree: ^9.3.0 - esquery: ^1.4.0 - esutils: ^2.0.2 - fast-deep-equal: ^3.1.3 - file-entry-cache: ^6.0.1 - functional-red-black-tree: ^1.0.1 - glob-parent: ^6.0.1 - globals: ^13.6.0 - ignore: ^5.2.0 - import-fresh: ^3.0.0 - imurmurhash: ^0.1.4 - is-glob: ^4.0.0 - js-yaml: ^4.1.0 - json-stable-stringify-without-jsonify: ^1.0.1 - levn: ^0.4.1 - lodash.merge: ^4.6.2 - minimatch: ^3.0.4 - natural-compare: ^1.4.0 - optionator: ^0.9.1 - regexpp: ^3.2.0 - strip-ansi: ^6.0.1 - strip-json-comments: ^3.1.0 - text-table: ^0.2.0 v8-compile-cache: ^2.0.3 bin: eslint: bin/eslint.js - checksum: 41a7e85bf84cf9f2d758ef3e8d08020a39a2836703728b59535684681349bd021c2c6e24174462b844a914870d707d2151e0371198899d957b444de91adaa435 + checksum: 2d84a7a2207138cdb250759b047fdb05a57fede7f87b7a039d9370edba7f26e23a873a208becfd4b2c9e4b5499029f3fc3b9318da3290e693d25c39084119c80 languageName: node linkType: hard @@ -17910,7 +17795,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.3.0, espree@npm:^9.3.2, espree@npm:^9.3.3": +"espree@npm:^9.3.2, espree@npm:^9.3.3": version: 9.3.3 resolution: "espree@npm:9.3.3" dependencies: @@ -20598,14 +20483,7 @@ __metadata: languageName: node linkType: hard -"highlight.js@npm:^11.5.1": - version: 11.5.1 - resolution: "highlight.js@npm:11.5.1" - checksum: bff556101d7691c6275ad19318e368fc971cd0621ef3d86222f5373df7d8191a2fc1ffd47f118138cbcf85e5fe91cfeefaecd6184f49a3ec18090955efc9edef - languageName: node - linkType: hard - -"highlight.js@npm:^11.6.0": +"highlight.js@npm:^11.5.1, highlight.js@npm:^11.6.0": version: 11.6.0 resolution: "highlight.js@npm:11.6.0" checksum: 3908eb34a4b442ca1e20c1ae6415ea935fbbcdb2b532a89948d82b0fa4ad41fc5de3802a0de4e88a0bcb7d97d4445579048cd2aab1d105ac47f59dd58a9a98ae @@ -25469,16 +25347,7 @@ __metadata: languageName: node linkType: hard -"memfs@npm:^3.1.2": - version: 3.4.6 - resolution: "memfs@npm:3.4.6" - dependencies: - fs-monkey: ^1.0.3 - checksum: 0164d79c5da42809d9590125ee713aac59b1c1e16c61d4b264460366514342da2867ac64874f098af348e13eadde4c6d8fb8188b16e766029d67adf8ec153b4c - languageName: node - linkType: hard - -"memfs@npm:^3.2.2": +"memfs@npm:^3.1.2, memfs@npm:^3.2.2": version: 3.4.7 resolution: "memfs@npm:3.4.7" dependencies: @@ -29602,7 +29471,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.0, postcss@npm:^8.2.15, postcss@npm:~8.4.16": +"postcss@npm:^8.0.0, postcss@npm:^8.2.15, postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:~8.4.14, postcss@npm:~8.4.16": version: 8.4.16 resolution: "postcss@npm:8.4.16" dependencies: @@ -29613,17 +29482,6 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:~8.4.14": - version: 8.4.14 - resolution: "postcss@npm:8.4.14" - dependencies: - nanoid: ^3.3.4 - picocolors: ^1.0.0 - source-map-js: ^1.0.2 - checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816 - languageName: node - linkType: hard - "postis@npm:^2.2.0": version: 2.2.0 resolution: "postis@npm:2.2.0" @@ -30797,7 +30655,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^17.0.2, react@npm:~17.0.2": +"react@npm:~17.0.2": version: 17.0.2 resolution: "react@npm:17.0.2" dependencies: @@ -34245,7 +34103,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.0.3": +"terser-webpack-plugin@npm:^5.0.3, terser-webpack-plugin@npm:^5.1.3": version: 5.3.5 resolution: "terser-webpack-plugin@npm:5.3.5" dependencies: @@ -34267,28 +34125,6 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.1.3": - version: 5.3.3 - resolution: "terser-webpack-plugin@npm:5.3.3" - dependencies: - "@jridgewell/trace-mapping": ^0.3.7 - jest-worker: ^27.4.5 - schema-utils: ^3.1.1 - serialize-javascript: ^6.0.0 - terser: ^5.7.2 - peerDependencies: - webpack: ^5.1.0 - peerDependenciesMeta: - "@swc/core": - optional: true - esbuild: - optional: true - uglify-js: - optional: true - checksum: 4b8d508d8a0f6e604addb286975f1fa670f8c3964a67abc03a7cfcfd4cdeca4b07dda6655e1c4425427fb62e4d2b0ca59d84f1b2cd83262ff73616d5d3ccdeb5 - languageName: node - linkType: hard - "terser@npm:^4.1.2, terser@npm:^4.6.3": version: 4.8.0 resolution: "terser@npm:4.8.0" @@ -34302,7 +34138,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.14.1": +"terser@npm:^5.10.0, terser@npm:^5.14.1, terser@npm:^5.3.4": version: 5.14.2 resolution: "terser@npm:5.14.2" dependencies: @@ -34316,20 +34152,6 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.3.4, terser@npm:^5.7.2": - version: 5.14.1 - resolution: "terser@npm:5.14.1" - dependencies: - "@jridgewell/source-map": ^0.3.2 - acorn: ^8.5.0 - commander: ^2.20.0 - source-map-support: ~0.5.20 - bin: - terser: bin/terser - checksum: 7b0e51f3d193a11cad82f07e3b0c1d62122eec786f809bdf2a54b865aaa1450872c3a7b6c33b5a40e264834060ffc1d4e197f971a76da5b0137997d756eb7548 - languageName: node - linkType: hard - "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -36523,44 +36345,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:>=4.43.0 <6.0.0": - version: 5.73.0 - resolution: "webpack@npm:5.73.0" - dependencies: - "@types/eslint-scope": ^3.7.3 - "@types/estree": ^0.0.51 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.4.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.9.3 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 - json-parse-even-better-errors: ^2.3.1 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.3.1 - webpack-sources: ^3.2.3 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: aa434a241bad6176b68e1bf0feb1972da4dcbf27cb3d94ae24f6eb31acc37dceb9c4aae55e068edca75817bfe91f13cd20b023ac55d9b1b2f8b66a4037c9468f - languageName: node - linkType: hard - -"webpack@npm:^5.9.0": +"webpack@npm:>=4.43.0 <6.0.0, webpack@npm:^5.9.0": version: 5.74.0 resolution: "webpack@npm:5.74.0" dependencies: -- GitLab From 18441519eda828af8e69ca58282fe515adc5594e Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:10:52 +0530 Subject: [PATCH 059/107] [FIX] Old rooms without the associated unit will not be displayed on the current chats (#26685) --- .../ee/app/license/server/getStatistics.ts | 45 ++++-- .../livechat-enterprise/server/api/units.ts | 6 +- .../server/hooks/addDepartmentAncestors.js | 3 +- .../server/hooks/afterForwardChatToAgent.ts | 9 +- .../hooks/afterForwardChatToDepartment.js | 9 +- .../server/hooks/afterReturnRoomAsInquiry.ts | 8 +- .../server/lib/LivechatEnterprise.js | 23 +-- .../server/lib/VisitorInactivityMonitor.js | 9 +- apps/meteor/ee/app/models/server/index.js | 1 - .../app/models/server/models/LivechatRooms.js | 28 +--- .../app/models/server/models/LivechatUnit.js | 9 +- .../ee/app/models/server/raw/LivechatRooms.js | 25 ---- apps/meteor/ee/server/models/LivechatRooms.ts | 6 + .../ee/server/models/raw/LivechatRooms.ts | 139 ++++++++++++++++++ apps/meteor/ee/server/models/startup.ts | 15 +- .../src/IOmnichannelBusinessUnit.ts | 5 +- packages/core-typings/src/IRoom.ts | 3 + packages/core-typings/src/IStats.ts | 8 +- .../src/models/ILivechatRoomsModel.ts | 4 +- 19 files changed, 246 insertions(+), 109 deletions(-) delete mode 100644 apps/meteor/ee/app/models/server/raw/LivechatRooms.js create mode 100644 apps/meteor/ee/server/models/LivechatRooms.ts create mode 100644 apps/meteor/ee/server/models/raw/LivechatRooms.ts diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index ece779f63c8..28d6b5c7100 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -2,13 +2,18 @@ import { log } from 'console'; import { CannedResponse, LivechatPriority, LivechatTag, LivechatUnit } from '@rocket.chat/models'; -import { getModules, getTags } from './license'; +import { getModules, getTags, hasLicense } from './license'; import { Analytics } from '../../../../server/sdk'; -type ENTERPRISE_STATISTICS = { +type ENTERPRISE_STATISTICS = GenericStats & Partial<EEOnlyStats>; + +type GenericStats = { modules: string[]; tags: string[]; seatRequests: number; +}; + +type EEOnlyStats = { livechatTags: number; cannedResponses: number; priorities: number; @@ -16,21 +21,32 @@ type ENTERPRISE_STATISTICS = { }; export async function getStatistics(): Promise<ENTERPRISE_STATISTICS> { - const statsPms: Array<Promise<any>> = []; + const genericStats: GenericStats = { + modules: getModules(), + tags: getTags().map(({ name }) => name), + seatRequests: await Analytics.getSeatRequestCount(), + }; - const statistics: ENTERPRISE_STATISTICS = {} as any; + const eeModelsStats = await getEEStatistics(); - const modules = getModules(); - statistics.modules = modules; + const statistics = { + ...genericStats, + ...eeModelsStats, + }; - const tags = getTags().map(({ name }) => name); - statistics.tags = tags; + return statistics; +} + +// These models are only available on EE license so don't import them inside CE license as it will break the build +async function getEEStatistics(): Promise<EEOnlyStats | undefined> { + if (!hasLicense('livechat-enterprise')) { + return; + } + + const statsPms: Array<Promise<any>> = []; + + const statistics: Partial<EEOnlyStats> = {}; - statsPms.push( - Analytics.getSeatRequestCount().then((count) => { - statistics.seatRequests = count; - }), - ); // Number of livechat tags statsPms.push( LivechatTag.col.count().then((count) => { @@ -66,5 +82,6 @@ export async function getStatistics(): Promise<ENTERPRISE_STATISTICS> { ); await Promise.all(statsPms).catch(log); - return statistics; + + return statistics as EEOnlyStats; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts index cac122fa83e..108f71eb470 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts @@ -1,3 +1,5 @@ +import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; + import { API } from '../../../../../app/api/server'; import { findUnits, findUnitById, findUnitMonitors } from './lib/units'; import { LivechatEnterprise } from '../lib/LivechatEnterprise'; @@ -44,7 +46,7 @@ API.v1.addRoute( }, async post() { const { unitData, unitMonitors, unitDepartments } = this.bodyParams; - return API.v1.success(LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments)); + return API.v1.success(LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments) as IOmnichannelBusinessUnit); }, }, ); @@ -65,7 +67,7 @@ API.v1.addRoute( const { unitData, unitMonitors, unitDepartments } = this.bodyParams; const { id } = this.urlParams; - return LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments); + return API.v1.success(LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments) as IOmnichannelBusinessUnit); }, async delete() { const { id } = this.urlParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/addDepartmentAncestors.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/addDepartmentAncestors.js index d8d56b6bf42..c5dbf47eb63 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/addDepartmentAncestors.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/addDepartmentAncestors.js @@ -1,5 +1,6 @@ +import { LivechatRooms } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; -import LivechatRooms from '../../../../../app/models/server/models/LivechatRooms'; import LivechatDepartment from '../../../../../app/models/server/models/LivechatDepartment'; callbacks.add( diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts index 1be1d40bfe2..9f90b3f830c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts @@ -1,17 +1,16 @@ +import { LivechatRooms } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; -import LivechatRooms from '../../../../../app/models/server/models/LivechatRooms'; callbacks.add( 'livechat.afterForwardChatToAgent', (options: { rid?: string } = {}) => { const { rid } = options; - - const room = LivechatRooms.findOneById(rid); - if (!room) { + if (!rid) { return options; } - (LivechatRooms as any).unsetPredictedVisitorAbandonmentByRoomId(rid); + Promise.await(LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(rid)); return options; }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js index 9b70a3e9d2e..c8dadd65d3e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js @@ -1,5 +1,6 @@ +import { LivechatRooms } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; -import LivechatRooms from '../../../../../app/models/server/models/LivechatRooms'; import LivechatDepartment from '../../../../../app/models/server/models/LivechatDepartment'; import { cbLogger } from '../lib/logger'; @@ -8,12 +9,12 @@ callbacks.add( (options) => { const { rid, newDepartmentId } = options; - const room = LivechatRooms.findOneById(rid); + const room = Promise.await(LivechatRooms.findOneById(rid)); if (!room) { cbLogger.debug('Skipping callback. No room found'); return options; } - LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); + Promise.await(LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id)); const department = LivechatDepartment.findOneById(newDepartmentId, { fields: { ancestors: 1 }, @@ -31,7 +32,7 @@ callbacks.add( } cbLogger.debug(`Updating department ${newDepartmentId} ancestors for room ${rid}`); - LivechatRooms.updateDepartmentAncestorsById(room._id, ancestors); + Promise.await(LivechatRooms.updateDepartmentAncestorsById(room._id, ancestors)); return options; }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts index ca4253b03a3..994516c1055 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterReturnRoomAsInquiry.ts @@ -1,15 +1,17 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; -import { LivechatRooms } from '../../../../../app/models/server'; import { settings } from '../../../../../app/settings/server'; import { cbLogger } from '../lib/logger'; -const unsetPredictedVisitorAbandonment = ({ room }: { room: any }): void => { +const unsetPredictedVisitorAbandonment = ({ room }: { room: IOmnichannelRoom }): void => { if (!room?._id || !room?.omnichannel?.predictedVisitorAbandonmentAt) { cbLogger.debug('Skipping callback. No room or no visitor abandonment info'); return; } - (LivechatRooms as any).unsetPredictedVisitorAbandonmentByRoomId(room._id); + Promise.await(LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id)); }; settings.watch('Livechat_abandoned_rooms_action', (value) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 3e0049f63eb..eb77e1dffed 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { LivechatInquiry } from '@rocket.chat/models'; +import { LivechatInquiry, Users } from '@rocket.chat/models'; import LivechatUnit from '../../../models/server/models/LivechatUnit'; import LivechatTag from '../../../models/server/models/LivechatTag'; -import { Users, LivechatRooms, Subscriptions, Messages } from '../../../../../app/models/server'; +import { LivechatRooms, Subscriptions, Messages } from '../../../../../app/models/server'; import LivechatPriority from '../../../models/server/models/LivechatPriority'; import { addUserRoles } from '../../../../../server/lib/roles/addUserRoles'; import { removeUserFromRoles } from '../../../../../server/lib/roles/removeUserFromRoles'; @@ -20,12 +20,13 @@ import { settings } from '../../../../../app/settings/server'; import { logger, queueLogger } from './logger'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoCloseOnHoldScheduler } from './AutoCloseOnHoldScheduler'; +import { LivechatUnitMonitors } from '../../../models/server'; export const LivechatEnterprise = { - addMonitor(username) { + async addMonitor(username) { check(username, String); - const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } }); + const user = await Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -40,10 +41,10 @@ export const LivechatEnterprise = { return false; }, - removeMonitor(username) { + async removeMonitor(username) { check(username, String); - const user = Users.findOneByUsername(username, { fields: { _id: 1 } }); + const user = await Users.findOneByUsername(username, { fields: { _id: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -51,11 +52,15 @@ export const LivechatEnterprise = { }); } - if (removeUserFromRoles(user._id, ['livechat-monitor'])) { - return true; + const removeRoleResult = removeUserFromRoles(user._id, ['livechat-monitor']); + if (!removeRoleResult) { + return false; } - return false; + // remove this monitor from any unit it is assigned to + LivechatUnitMonitors.removeByMonitorId(user._id); + + return true; }, removeUnit(_id) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index e4b22980313..b794faf810b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -1,10 +1,10 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Meteor } from 'meteor/meteor'; -import { LivechatVisitors } from '@rocket.chat/models'; +import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; -import { LivechatRooms, LivechatDepartment, Users } from '../../../../../app/models/server'; +import { LivechatDepartment, Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; import { LivechatEnterprise } from './LivechatEnterprise'; @@ -91,7 +91,8 @@ export class VisitorInactivityMonitor { const guest = visitor.name || visitor.username; const comment = TAPi18n.__('Omnichannel_On_Hold_due_to_inactivity', { guest, timeout }); - LivechatEnterprise.placeRoomOnHold(room, comment, this.user) && LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); + LivechatEnterprise.placeRoomOnHold(room, comment, this.user) && + (await LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id)); } handleAbandonedRooms() { @@ -99,7 +100,7 @@ export class VisitorInactivityMonitor { if (!action || action === 'none') { return; } - LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => { + Promise.await(LivechatRooms.findAbandonedOpenRooms(new Date())).forEach((room) => { switch (action) { case 'close': { this.closeRooms(room); diff --git a/apps/meteor/ee/app/models/server/index.js b/apps/meteor/ee/app/models/server/index.js index c8424f0163a..ea7498ee2e0 100644 --- a/apps/meteor/ee/app/models/server/index.js +++ b/apps/meteor/ee/app/models/server/index.js @@ -8,7 +8,6 @@ import './models/LivechatRooms'; import './models/LivechatInquiry'; import './models/Messages'; import './models/Users'; -import './raw/LivechatRooms'; import './raw/LivechatDepartmentAgents'; export { CannedResponse, LivechatTag, LivechatUnit, LivechatUnitMonitors, LivechatPriority }; diff --git a/apps/meteor/ee/app/models/server/models/LivechatRooms.js b/apps/meteor/ee/app/models/server/models/LivechatRooms.js index 73e810af90d..4247a642266 100644 --- a/apps/meteor/ee/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/ee/app/models/server/models/LivechatRooms.js @@ -14,15 +14,9 @@ overwriteClassOnLicense('livechat-enterprise', LivechatRooms, { find: applyRestrictions('find'), update: applyRestrictions('update'), remove: applyRestrictions('remove'), - updateDepartmentAncestorsById(originalFn, _id, departmentAncestors) { - const query = { - _id, - }; - const update = departmentAncestors ? { $set: { departmentAncestors } } : { $unset: { departmentAncestors: 1 } }; - return this.update(query, update); - }, }); +// TODO: Engineering day - Move all these methods to the LivechatRooms model - apps/meteor/ee/server/models/raw/LivechatRooms.ts LivechatRooms.prototype.setPredictedVisitorAbandonment = function (roomId, willBeAbandonedAt) { const query = { _id: roomId, @@ -36,15 +30,6 @@ LivechatRooms.prototype.setPredictedVisitorAbandonment = function (roomId, willB return this.update(query, update); }; -LivechatRooms.prototype.findAbandonedOpenRooms = function (date) { - return this.find({ - 'omnichannel.predictedVisitorAbandonmentAt': { $lte: date }, - 'waitingResponse': { $exists: false }, - 'closedAt': { $exists: false }, - 'open': true, - }); -}; - LivechatRooms.prototype.setOnHold = function (roomId) { return this.update({ _id: roomId }, { $set: { onHold: true } }); }; @@ -69,17 +54,6 @@ LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function () { ); }; -LivechatRooms.prototype.unsetPredictedVisitorAbandonmentByRoomId = function (roomId) { - return this.update( - { - _id: roomId, - }, - { - $unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 }, - }, - ); -}; - LivechatRooms.prototype.unsetAllOnHoldFieldsByRoomId = function (roomId) { return this.update( { diff --git a/apps/meteor/ee/app/models/server/models/LivechatUnit.js b/apps/meteor/ee/app/models/server/models/LivechatUnit.js index c8218679a93..b6a4855eb0c 100644 --- a/apps/meteor/ee/app/models/server/models/LivechatUnit.js +++ b/apps/meteor/ee/app/models/server/models/LivechatUnit.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { LivechatRooms } from '@rocket.chat/models'; import LivechatDepartmentInstance, { LivechatDepartment } from '../../../../../app/models/server/models/LivechatDepartment'; import { getUnitsFromUser } from '../../../livechat-enterprise/server/lib/units'; @@ -105,7 +106,12 @@ export class LivechatUnit extends LivechatDepartment { ); }); - return _.extend(record, { _id }); + Promise.await(LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id)); + + return { + ...record, + ...(_id && { _id }), + }; } // REMOVE @@ -129,6 +135,7 @@ export class LivechatUnit extends LivechatDepartment { removeById(_id) { LivechatUnitMonitors.removeByUnitId(_id); this.removeParentAndAncestorById(_id); + Promise.await(LivechatRooms.removeUnitAssociationFromRooms(_id)); const query = { _id }; return this.remove(query); diff --git a/apps/meteor/ee/app/models/server/raw/LivechatRooms.js b/apps/meteor/ee/app/models/server/raw/LivechatRooms.js deleted file mode 100644 index bc7ee28ac5e..00000000000 --- a/apps/meteor/ee/app/models/server/raw/LivechatRooms.js +++ /dev/null @@ -1,25 +0,0 @@ -import { LivechatRoomsRaw } from '../../../../../server/models/raw/LivechatRooms'; -import { queriesLogger } from '../../../livechat-enterprise/server/lib/logger'; -import { addQueryRestrictionsToRoomsModel } from '../../../livechat-enterprise/server/lib/query.helper'; -import { overwriteClassOnLicense } from '../../../license/server'; - -const applyRestrictions = (method) => - function (originalFn, originalQuery, ...args) { - const query = addQueryRestrictionsToRoomsModel(originalQuery); - queriesLogger.debug({ msg: `LivechatRoomsRaw.${method}`, query }); - return originalFn.call(this, query, ...args); - }; - -overwriteClassOnLicense('livechat-enterprise', LivechatRoomsRaw, { - find: applyRestrictions('find'), - update: applyRestrictions('update'), - findPaginated: applyRestrictions('findPaginated'), - remove: applyRestrictions('remove'), - updateDepartmentAncestorsById(originalFn, _id, departmentAncestors) { - const query = { - _id, - }; - const update = departmentAncestors ? { $set: { departmentAncestors } } : { $unset: { departmentAncestors: 1 } }; - return this.update(query, update); - }, -}); diff --git a/apps/meteor/ee/server/models/LivechatRooms.ts b/apps/meteor/ee/server/models/LivechatRooms.ts new file mode 100644 index 00000000000..af883e548cc --- /dev/null +++ b/apps/meteor/ee/server/models/LivechatRooms.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../../../server/database/utils'; +import { LivechatRoomsRawEE } from './raw/LivechatRooms'; + +registerModel('ILivechatRoomsModel', new LivechatRoomsRawEE(db)); diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts new file mode 100644 index 00000000000..99c9775a14b --- /dev/null +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -0,0 +1,139 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ILivechatRoomsModel } from '@rocket.chat/model-typings'; +import type { FindCursor, UpdateResult } from 'mongodb'; + +import { LivechatRoomsRaw } from '../../../../server/models/raw/LivechatRooms'; +import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; +import { addQueryRestrictionsToRoomsModel } from '../../../app/livechat-enterprise/server/lib/query.helper'; + +declare module '@rocket.chat/model-typings' { + export interface ILivechatRoomsModel { + associateRoomsWithDepartmentToUnit: (departments: string[], unit: string) => Promise<void>; + removeUnitAssociationFromRooms: (unit: string) => Promise<void>; + updateDepartmentAncestorsById: (unitId: string, ancestors: string[]) => Promise<void>; + unsetPredictedVisitorAbandonmentByRoomId(rid: string): Promise<UpdateResult>; + findAbandonedOpenRooms(date: Date): Promise<FindCursor<IOmnichannelRoom>>; + } +} + +export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoomsModel { + async findAbandonedOpenRooms(date: Date): Promise<FindCursor<IOmnichannelRoom>> { + return this.find({ + 'omnichannel.predictedVisitorAbandonmentAt': { $lte: date }, + 'waitingResponse': { $exists: false }, + 'closedAt': { $exists: false }, + 'open': true, + }); + } + + async unsetPredictedVisitorAbandonmentByRoomId(roomId: string): Promise<UpdateResult> { + return this.updateOne( + { + _id: roomId, + }, + { + $unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 }, + }, + ); + } + + async associateRoomsWithDepartmentToUnit(departments: string[], unitId: string): Promise<void> { + const query = { + $and: [ + { + departmentId: { $in: departments }, + }, + { + $or: [ + { + departmentAncestors: { $exists: false }, + }, + { + $and: [ + { + departmentAncestors: { $exists: true }, + }, + { + departmentAncestors: { $ne: unitId }, + }, + ], + }, + ], + }, + ], + }; + const update = { $set: { departmentAncestors: [unitId] } }; + queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, query, update }); + const associationResult = await this.updateMany(query, update); + queriesLogger.debug({ msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - association step`, result: associationResult }); + + const queryToDisassociateOldRoomsConnectedToUnit = { + departmentAncestors: unitId, + departmentId: { $nin: departments }, + }; + const updateToDisassociateRooms = { $unset: { departmentAncestors: 1 } }; + queriesLogger.debug({ + msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, + query: queryToDisassociateOldRoomsConnectedToUnit, + update: updateToDisassociateRooms, + }); + const disassociationResult = await this.updateMany(queryToDisassociateOldRoomsConnectedToUnit, updateToDisassociateRooms); + queriesLogger.debug({ + msg: `LivechatRoomsRawEE.associateRoomsWithDepartmentToUnit - disassociation step`, + result: disassociationResult, + }); + } + + async removeUnitAssociationFromRooms(unitId: string): Promise<void> { + const query = { + departmentAncestors: unitId, + }; + const update = { $unset: { departmentAncestors: 1 } }; + queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, query, update }); + const result = await this.updateMany(query, update); + queriesLogger.debug({ msg: `LivechatRoomsRawEE.removeUnitAssociationFromRooms`, result }); + } + + async updateDepartmentAncestorsById(unitId: string, departmentAncestors: string[]) { + const query = { + _id: unitId, + }; + const update = departmentAncestors ? { $set: { departmentAncestors } } : { $unset: { departmentAncestors: 1 } }; + await this.update(query, update); + } + + find(...args: Parameters<LivechatRoomsRaw['find']>) { + const [query, ...restArgs] = args; + const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + queriesLogger.debug({ msg: 'LivechatRoomsRawEE.find', query: restrictedQuery }); + return super.find(restrictedQuery, ...restArgs); + } + + findPaginated(...args: Parameters<LivechatRoomsRaw['findPaginated']>) { + const [query, ...restArgs] = args; + const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + queriesLogger.debug({ msg: 'LivechatRoomsRawEE.findPaginated', query: restrictedQuery }); + return super.findPaginated(restrictedQuery, ...restArgs); + } + + update(...args: Parameters<LivechatRoomsRaw['update']>) { + const [query, ...restArgs] = args; + const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + queriesLogger.debug({ msg: 'LivechatRoomsRawEE.update', query: restrictedQuery }); + return super.update(restrictedQuery, ...restArgs); + } + + updateOne(...args: Parameters<LivechatRoomsRaw['updateOne']>) { + const [query, ...restArgs] = args; + const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); + return super.updateOne(restrictedQuery, ...restArgs); + } + + updateMany(...args: Parameters<LivechatRoomsRaw['updateMany']>) { + const [query, ...restArgs] = args; + const restrictedQuery = addQueryRestrictionsToRoomsModel(query); + queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateMany', query: restrictedQuery }); + return super.updateMany(restrictedQuery, ...restArgs); + } +} diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 59a80fb1180..9645c9cd050 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,5 +1,10 @@ -import './CannedResponse'; -import './LivechatPriority'; -import './LivechatTag'; -import './LivechatUnit'; -import './LivechatUnitMonitors'; +import { onLicense } from '../../app/license/server/license'; + +onLicense('livechat-enterprise', () => { + import('./CannedResponse'); + import('./LivechatPriority'); + import('./LivechatTag'); + import('./LivechatUnit'); + import('./LivechatUnitMonitors'); + import('./LivechatRooms'); +}); diff --git a/packages/core-typings/src/IOmnichannelBusinessUnit.ts b/packages/core-typings/src/IOmnichannelBusinessUnit.ts index 9aacd2219ef..c9f2a3b7377 100644 --- a/packages/core-typings/src/IOmnichannelBusinessUnit.ts +++ b/packages/core-typings/src/IOmnichannelBusinessUnit.ts @@ -1,9 +1,10 @@ -export interface IOmnichannelBusinessUnit { +import type { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IOmnichannelBusinessUnit extends IRocketChatRecord { _id: string; name: string; visibility: 'public' | 'private'; type: string; numMonitors: number; numDepartments: number; - _updatedAt: Date; } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 6f3876ba0ae..289e48294eb 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -191,6 +191,9 @@ export interface IOmnichannelGenericRoom extends Omit<IRoom, 'default' | 'featur export interface IOmnichannelRoom extends IOmnichannelGenericRoom { t: 'l'; + omnichannel?: { + predictedVisitorAbandonmentAt: Date; + }; } export interface IVoipRoom extends IOmnichannelGenericRoom { diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 14c81472cdb..528681a381a 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -133,10 +133,10 @@ export interface IStats { modules: string[]; tags: string[]; seatRequests: number; - livechatTags: number; - cannedResponses: number; - priorities: number; - businessUnits: number; + livechatTags?: number; + cannedResponses?: number; + priorities?: number; + businessUnits?: number; }; createdAt: Date | string; totalOTR: number; diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index b5ed6975056..1e1a8d105e1 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,4 @@ -import type { IRocketChatRecord } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; @@ -19,7 +19,7 @@ type WithOptions = { options?: any; }; -export interface ILivechatRoomsModel extends IBaseModel<IRocketChatRecord> { +export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> { getQueueMetrics(params: { departmentId: any; agentId: any; includeOfflineAgents: any; options?: any }): any; findAllNumberOfAbandonedRooms(params: Period & WithDepartment & WithOnlyCount & WithOptions): Promise<any>; -- GitLab From 55e11c4afc273e7cfb735aed8835797a066f4405 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Thu, 22 Sep 2022 19:07:36 -0300 Subject: [PATCH 060/107] [FIX] Ephemeral messages not respecting katex setting (#26812) --- .../server/modules/listeners/listeners.module.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 1380756a17d..0f546ac8d50 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -40,10 +40,12 @@ export class ListenersModule { message.md = parse(message.msg, { colors: settings.get('HexColorPreview_Enabled'), emoticons: true, - katex: { - dollarSyntax: settings.get('Katex_Dollar_Syntax'), - parenthesisSyntax: settings.get('Katex_Parenthesis_Syntax'), - }, + ...(settings.get('Katex_Enabled') && { + katex: { + dollarSyntax: settings.get('Katex_Dollar_Syntax'), + parenthesisSyntax: settings.get('Katex_Parenthesis_Syntax'), + }, + }), }); } -- GitLab From 548d8e6fbe8d57f0d4c92dbf0af867c5c6277c08 Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Thu, 22 Sep 2022 20:14:05 -0300 Subject: [PATCH 061/107] [FIX] Message sequential after message thread preview (#26900) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .../MessageList/lib/isMessageSequential.ts | 8 +++-- .../MessageList/lib/isMessageNewDay.spec.ts | 12 ++++++++ .../lib/isMessageSequential.spec.ts | 30 +++++++++++++++++++ yarn.lock | 2 +- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.ts b/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.ts index 10c860bb966..3c5c9fa3d55 100644 --- a/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.ts +++ b/apps/meteor/client/views/room/MessageList/lib/isMessageSequential.ts @@ -2,6 +2,7 @@ import { IMessage } from '@rocket.chat/core-typings'; import { differenceInSeconds } from 'date-fns'; import { MessageTypes } from '../../../../../app/ui-utils/lib/MessageTypes'; +import { isMessageNewDay } from './isMessageNewDay'; export const isMessageSequential = (current: IMessage, previous: IMessage | undefined, groupingRange: number): boolean => { if (!previous) { @@ -16,6 +17,10 @@ export const isMessageSequential = (current: IMessage, previous: IMessage | unde return [previous.tmid, previous._id].includes(current.tmid); } + if (previous.tmid) { + return false; + } + if (current.groupable === false) { return false; } @@ -27,6 +32,5 @@ export const isMessageSequential = (current: IMessage, previous: IMessage | unde if (current.alias !== previous.alias) { return false; } - - return differenceInSeconds(current.ts, previous.ts) < groupingRange; + return differenceInSeconds(current.ts, previous.ts) < groupingRange && !isMessageNewDay(current, previous); }; diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts index b36d9d49e9f..27af3d4c6de 100644 --- a/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageNewDay.spec.ts @@ -46,4 +46,16 @@ describe('isMessageNewDay', () => { }; expect(isMessageNewDay(message, undefined)).to.be.true; }); + + it('should return true for different days even if the range is on second', () => { + const previous: IMessage = { + ...baseMessage, + ts: new Date(2022, 0, 1, 23, 59, 59, 999), + }; + const current: IMessage = { + ...baseMessage, + ts: new Date(2022, 0, 2, 0, 0, 0, 0), + }; + expect(isMessageNewDay(current, previous)).to.be.true; + }); }); diff --git a/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts index 35cffceb306..aa74388f04f 100644 --- a/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts +++ b/apps/meteor/tests/unit/client/views/room/MessageList/lib/isMessageSequential.spec.ts @@ -2,6 +2,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; +import { MessageTypes } from '../../../../../../../app/ui-utils/lib/MessageTypes'; import { isMessageSequential } from '../../../../../../../client/views/room/MessageList/lib/isMessageSequential'; const TIME_RANGE_IN_SECONDS = 300; @@ -101,6 +102,18 @@ describe('isMessageSequential', () => { expect(isMessageSequential(current, previous, TIME_RANGE_IN_SECONDS)).to.be.true; }); + it('should return false if previous message is thread message but the current is a regular one', () => { + const previous: IMessage = { + tmid: 'threadId', + ...baseMessage, + }; + const current: IMessage = { + ...baseMessage, + }; + + expect(isMessageSequential(current, previous, TIME_RANGE_IN_SECONDS)).to.be.false; + }); + it('should return true if message is a reply from a previous message', () => { const previous: IMessage = { ...baseMessage, @@ -125,6 +138,11 @@ describe('isMessageSequential', () => { }); it('should return false if message is from system', () => { + MessageTypes.registerType({ + id: 'au', + system: true, + message: 'User_added_by', + }); const previous: IMessage = { ...baseMessage, }; @@ -135,4 +153,16 @@ describe('isMessageSequential', () => { }; expect(isMessageSequential(current, previous, TIME_RANGE_IN_SECONDS)).to.be.false; }); + + it('should return false even if messages should be sequential, but they are from a different day', () => { + const previous: IMessage = { + ...baseMessage, + ts: new Date(2022, 0, 1, 23, 59, 59, 999), + }; + const current: IMessage = { + ...baseMessage, + ts: new Date(2022, 0, 2, 0, 0, 0, 0), + }; + expect(isMessageSequential(current, previous, TIME_RANGE_IN_SECONDS)).to.be.false; + }); }); diff --git a/yarn.lock b/yarn.lock index f7423fcc207..bcd37ca9f06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35954,7 +35954,7 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.11": +"vm2@npm:^3.9.10": version: 3.9.11 resolution: "vm2@npm:3.9.11" dependencies: -- GitLab From 9625a994b87741a0fb28748fbbdbd61709b4f6ba Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Thu, 22 Sep 2022 23:43:23 -0300 Subject: [PATCH 062/107] Chore: fix regressions for omnichannel due room refactor (#26912) --- .../client/models/CachedCollection.js | 5 ----- .../client/messageBox/messageBox.ts | 4 +++- .../NotAvailable.stories.tsx | 12 ----------- .../NotAvailableContent.tsx | 18 ----------------- .../NotAvailableContentWrapper.tsx | 12 ----------- .../NotAvailableOnCall/NotAvailableOnCall.tsx | 20 ------------------- .../voip/composer/NotAvailableOnCall/index.ts | 1 - .../client/lib/rooms/roomCoordinator.ts | 7 ++++++- .../components/Toolbox/Toolbox.tsx | 9 +++------ .../providers/MessageListProvider.tsx | 5 +++-- .../views/room/components/body/RoomBody.tsx | 7 ++++--- .../body/composer/ComposerContainer.tsx | 9 ++++++++- .../components/body/composer/ComposerVoIP.tsx | 15 ++++++++++++++ .../views/room/providers/ToolboxProvider.tsx | 1 + 14 files changed, 43 insertions(+), 82 deletions(-) delete mode 100644 apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailable.stories.tsx delete mode 100644 apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContent.tsx delete mode 100644 apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContentWrapper.tsx delete mode 100644 apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableOnCall.tsx delete mode 100644 apps/meteor/client/components/voip/composer/NotAvailableOnCall/index.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerVoIP.tsx diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js index 0d047fe2dd2..00750726c84 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js @@ -275,7 +275,6 @@ export class CachedCollection extends Emitter { } async setupListener(eventType, eventName) { - const { RoomManager } = await import('../../../ui-utils'); const { ChatRoom, CachedChatRoom } = await import('../../../models'); Notifications[eventType || this.eventType](eventName || this.eventName, (t, record) => { this.log('record received', t, record); @@ -292,10 +291,6 @@ export class CachedCollection extends Emitter { _id: record._id, }); } - if (room) { - room.name && RoomManager.close(room.t + room.name); - !room.name && RoomManager.close(room.t + room._id); - } this.collection.remove(record._id); } else { const { _id, ...recordData } = record; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 33e44e77b5a..9620030fda2 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -298,7 +298,9 @@ Template.messageBox.helpers({ isFederatedRoom() { const { rid } = Template.currentData(); - return isRoomFederated(ChatRoom.findOne(rid)); + const room = ChatRoom.findOne(rid); + + return room && isRoomFederated(room); }, }); diff --git a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailable.stories.tsx b/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailable.stories.tsx deleted file mode 100644 index 8b672ecab7b..00000000000 --- a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailable.stories.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import NotAvailableOnCall from './NotAvailableOnCall'; - -export default { - title: 'Components/VoIP/Composer/NotAvailableOnCall', - component: NotAvailableOnCall, -} as ComponentMeta<typeof NotAvailableOnCall>; - -export const Default: ComponentStory<typeof NotAvailableOnCall> = () => <NotAvailableOnCall />; -Default.storyName = 'NotAvailableOnCall'; diff --git a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContent.tsx b/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContent.tsx deleted file mode 100644 index 22e241f3fbe..00000000000 --- a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContent.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; -import React, { ComponentProps, ReactElement } from 'react'; - -type ContentPropsType = { - icon: ComponentProps<typeof Icon>['name']; - text: string; -}; - -const NotAvailableContent = ({ icon, text }: ContentPropsType): ReactElement => ( - <Box w='full' h='full' display='flex' flexDirection='row' alignItems='center' justifyContent='center'> - <Icon color='info' size='24px' name={icon} /> - <Box mis='4px' fontScale='p2' color='info'> - {text} - </Box> - </Box> -); - -export default NotAvailableContent; diff --git a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContentWrapper.tsx b/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContentWrapper.tsx deleted file mode 100644 index bdb8d9716e9..00000000000 --- a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableContentWrapper.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { ReactElement } from 'react'; - -type NotAvailableProps = { - children: ReactElement; -}; - -const NotAvailableContentWrapper = (props: NotAvailableProps): ReactElement => ( - <Box w='full' h='full' backgroundColor='neutral-100' {...props} /> -); - -export default NotAvailableContentWrapper; diff --git a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableOnCall.tsx b/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableOnCall.tsx deleted file mode 100644 index c7ec9d722c0..00000000000 --- a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/NotAvailableOnCall.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { ReactElement } from 'react'; - -import NotAvailableContent from './NotAvailableContent'; -import NotAvailableContentWrapper from './NotAvailableContentWrapper'; - -const NotAvailableOnCall = (): ReactElement => { - const t = useTranslation(); - - return ( - <Box h='44px' w='100%' borderColor='disabled' borderWidth='2px'> - <NotAvailableContentWrapper> - <NotAvailableContent icon='message' text={t('Composer_not_available_phone_calls')} /> - </NotAvailableContentWrapper> - </Box> - ); -}; - -export default NotAvailableOnCall; diff --git a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/index.ts b/apps/meteor/client/components/voip/composer/NotAvailableOnCall/index.ts deleted file mode 100644 index 511ccb4231c..00000000000 --- a/apps/meteor/client/components/voip/composer/NotAvailableOnCall/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './NotAvailableOnCall'; diff --git a/apps/meteor/client/lib/rooms/roomCoordinator.ts b/apps/meteor/client/lib/rooms/roomCoordinator.ts index db650a40810..f9dd38b47c2 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.ts +++ b/apps/meteor/client/lib/rooms/roomCoordinator.ts @@ -9,6 +9,7 @@ import { openRoom } from '../../../app/ui-utils/client/lib/openRoom'; import { RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../../../definition/IRoomTypeConfig'; import type { IRoomTypeConfig, IRoomTypeClientDirectives, RoomIdentification } from '../../../definition/IRoomTypeConfig'; import { RoomCoordinator } from '../../../lib/rooms/coordinator'; +import { ToolboxContextValue } from '../../views/room/lib/Toolbox/ToolboxContext'; import { roomExit } from './roomExit'; class RoomCoordinatorClient extends RoomCoordinator { @@ -26,7 +27,11 @@ class RoomCoordinatorClient extends RoomCoordinator { isGroupChat(_room: Partial<IRoom>): boolean { return false; }, - openCustomProfileTab(_instance: any, _room: IRoom, _username: string): boolean { + openCustomProfileTab< + T extends { + tabBar: ToolboxContextValue; + }, + >(_instance: T, _room: IRoom, _username: string): boolean { return false; }, getUiText(_context: ValueOf<typeof UiTextContext>): string { diff --git a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx index 0632eadd93c..fd9e259c67b 100644 --- a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx +++ b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx @@ -1,9 +1,10 @@ import { IMessage, isRoomFederated, IUser } from '@rocket.chat/core-typings'; import { MessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; -import { useUser, useUserRoom, useUserSubscription, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUser, useUserSubscription, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, memo, useMemo } from 'react'; import { MessageAction } from '../../../../../../app/ui-utils/client/lib/MessageAction'; +import { useRoom } from '../../../contexts/RoomContext'; import { getTabBarContext } from '../../../lib/Toolbox/ToolboxContext'; import { useIsSelecting } from '../../contexts/SelectedMessagesContext'; import { MessageActionMenu } from './MessageActionMenu'; @@ -11,11 +12,7 @@ import { MessageActionMenu } from './MessageActionMenu'; export const Toolbox: FC<{ message: IMessage }> = ({ message }) => { const t = useTranslation(); - const room = useUserRoom(message.rid); - - if (!room) { - throw new Error('Room not found'); - } + const room = useRoom(); const subscription = useUserSubscription(message.rid); const settings = useSettings(); diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index c42c5ec3ba8..77b6c103988 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -6,12 +6,13 @@ import { MessageAttachment, isThreadMainMessage, } from '@rocket.chat/core-typings'; -import { useLayout, useUser, useUserPreference, useUserSubscription, useSetting, useEndpoint, useUserRoom } from '@rocket.chat/ui-contexts'; +import { useLayout, useUser, useUserPreference, useUserSubscription, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { useMemo, FC, memo } from 'react'; import { AutoTranslate } from '../../../../../app/autotranslate/client'; import { EmojiPicker } from '../../../../../app/emoji/client'; import { getRegexHighlight, getRegexHighlightUrl } from '../../../../../app/highlight-words/client/helper'; +import { useRoom } from '../../contexts/RoomContext'; import ToolboxProvider from '../../providers/ToolboxProvider'; import { MessageListContext, MessageListContextValue } from '../contexts/MessageListContext'; import { useAutotranslateLanguage } from '../hooks/useAutotranslateLanguage'; @@ -166,7 +167,7 @@ export const MessageListProvider: FC<{ ], ); - const room = useUserRoom(rid); + const room = useRoom(); if (!room) { throw new Error('Room not found'); diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 0f9beecf4c0..3b633719d34 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -276,11 +276,12 @@ const RoomBody = (): ReactElement => { return; } - if (room && isOmnichannelRoom(room)) { - roomCoordinator.getRoomDirectives(room.t)?.openCustomProfileTab(null, room, room.v.username); + if (room && isOmnichannelRoom(room) && !tabBar.activeTabBar) { + roomCoordinator.getRoomDirectives(room.t)?.openCustomProfileTab({ tabBar }, room, room.v.username); } }); - }, [openedRoom, room, room._id]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [openedRoom, room._id]); const debouncedReadMessageRead = useMemo( () => diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx index 5b5669662ab..adb8f9d18f2 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx @@ -8,6 +8,7 @@ import { ComposerJoinWithPassword } from './ComposerJoinWithPassword'; import ComposerMessage, { ComposerMessageProps } from './ComposerMessage'; import { ComposerOmnichannel } from './ComposerOmnichannel/ComposerOmnichannel'; import { ComposerReadOnly } from './ComposerReadOnly'; +import ComposerVoIP from './ComposerVoIP'; import { useMessageComposerIsAnonymous } from './hooks/useMessageComposerIsAnonymous'; import { useMessageComposerIsBlocked } from './hooks/useMessageComposerIsBlocked'; import { useMessageComposerIsReadOnly } from './hooks/useMessageComposerIsReadOnly'; @@ -23,12 +24,18 @@ const ComposerContainer = (props: ComposerMessageProps): ReactElement => { const isReadOnly = useMessageComposerIsReadOnly(props.rid, props.subscription); - const isOmnichannel = isOmnichannelRoom(room) || isVoipRoom(room); + const isOmnichannel = isOmnichannelRoom(room); + + const isVoip = isVoipRoom(room); if (isOmnichannel) { return <ComposerOmnichannel {...props} />; } + if (isVoip) { + return <ComposerVoIP />; + } + if (isAnonymous) { return ( <footer className='rc-message-box footer'> diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerVoIP.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerVoIP.tsx new file mode 100644 index 00000000000..3b0a1a6cd9f --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerVoIP.tsx @@ -0,0 +1,15 @@ +import { MessageFooterCallout } from '@rocket.chat/ui-composer'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +const ComposerVoIP = (): ReactElement => { + const t = useTranslation(); + + return ( + <footer className='rc-message-box footer'> + <MessageFooterCallout>{t('Composer_not_available_phone_calls')}</MessageFooterCallout> + </footer> + ); +}; + +export default ComposerVoIP; diff --git a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx index 647e18f221f..d55344d711f 100644 --- a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx @@ -91,6 +91,7 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom setActiveTabBar([list.get(tab as string) as ToolboxActionConfig, context]); }, [tab, list, currentRoom, context]); + // TODO: make the object stable, preventing re-renders const contextValue = useMemo( () => ({ listen, -- GitLab From 4ab9c35f611088f3f42da645265c3b93a47451de Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Thu, 22 Sep 2022 23:43:57 -0300 Subject: [PATCH 063/107] Chore: Move micro services to packages (#26884) --- .github/workflows/build_and_test.yml | 218 ++++++++++-------- .../app/statistics/server/lib/statistics.ts | 2 + .../admin/info/DeploymentCard.stories.tsx | 1 + .../views/admin/info/DeploymentCard.tsx | 6 +- .../admin/info/InformationPage.stories.tsx | 1 + .../views/admin/info/InformationPage.tsx | 8 +- .../views/admin/info/UsageCard.stories.tsx | 1 + apps/meteor/ee/server/NetworkBroker.ts | 19 +- .../ee/server/services/account/Account.ts | 147 ------------ .../ee/server/services/account/service.ts | 6 - .../server/services/authorization/service.ts | 16 -- .../ee/server/services/ecosystem.config.js | 33 --- apps/meteor/ee/server/services/package.json | 4 - .../server/services/stream-hub/StreamHub.ts | 20 -- .../ee/server/services/stream-hub/service.ts | 18 -- apps/meteor/ee/server/services/tsconfig.json | 2 +- apps/meteor/ee/server/startup/broker.ts | 3 +- apps/meteor/ee/server/startup/index.ts | 8 +- apps/meteor/server/sdk/api.ts | 1 + apps/meteor/server/sdk/lib/Api.ts | 7 + apps/meteor/server/sdk/lib/LocalBroker.ts | 10 +- apps/meteor/server/sdk/types/IBroker.ts | 1 + .../server/services/authorization/service.ts | 11 +- apps/meteor/server/services/startup.ts | 2 +- apps/meteor/server/startup/serverRunning.js | 4 +- docker-compose-ci.yml | 16 +- ee/apps/account-service/.eslintrc | 16 ++ ee/apps/account-service/Dockerfile | 34 +++ ee/apps/account-service/package.json | 50 ++++ ee/apps/account-service/src/Account.ts | 53 +++++ .../account-service/src/lib/loginViaResume.ts | 39 ++++ .../src/lib/loginViaUsername.ts | 36 +++ .../account-service/src/lib/removeSession.ts | 14 ++ .../account-service/src/lib/saveSession.ts | 14 ++ .../apps/account-service/src}/lib/utils.ts | 0 ee/apps/account-service/src/service.ts | 33 +++ ee/apps/account-service/tsconfig.json | 31 +++ ee/apps/authorization-service/.eslintrc | 16 ++ ee/apps/authorization-service/Dockerfile | 34 +++ ee/apps/authorization-service/package.json | 47 ++++ ee/apps/authorization-service/src/service.ts | 33 +++ ee/apps/authorization-service/tsconfig.json | 31 +++ ee/apps/ddp-streamer/Dockerfile | 2 - ee/apps/ddp-streamer/package.json | 5 +- ee/apps/ddp-streamer/src/DDPStreamer.ts | 114 +++++---- ee/apps/ddp-streamer/src/proxy.ts | 41 ++++ ee/apps/ddp-streamer/src/service.ts | 29 ++- ee/apps/ddp-streamer/src/streams.ts | 16 -- ee/apps/presence-service/package.json | 5 +- ee/apps/presence-service/src/service.ts | 13 +- ee/apps/presence-service/tsconfig.json | 2 +- ee/apps/stream-hub-service/.eslintrc | 16 ++ ee/apps/stream-hub-service/Dockerfile | 34 +++ ee/apps/stream-hub-service/package.json | 47 ++++ ee/apps/stream-hub-service/src/StreamHub.ts | 22 ++ ee/apps/stream-hub-service/src/service.ts | 33 +++ ee/apps/stream-hub-service/tsconfig.json | 31 +++ packages/core-typings/src/IStats.ts | 1 + packages/presence/package.json | 1 - yarn.lock | 119 +++++++++- 60 files changed, 1102 insertions(+), 475 deletions(-) delete mode 100644 apps/meteor/ee/server/services/account/Account.ts delete mode 100644 apps/meteor/ee/server/services/account/service.ts delete mode 100644 apps/meteor/ee/server/services/authorization/service.ts delete mode 100644 apps/meteor/ee/server/services/ecosystem.config.js delete mode 100755 apps/meteor/ee/server/services/stream-hub/StreamHub.ts delete mode 100755 apps/meteor/ee/server/services/stream-hub/service.ts create mode 100644 ee/apps/account-service/.eslintrc create mode 100644 ee/apps/account-service/Dockerfile create mode 100644 ee/apps/account-service/package.json create mode 100644 ee/apps/account-service/src/Account.ts create mode 100644 ee/apps/account-service/src/lib/loginViaResume.ts create mode 100644 ee/apps/account-service/src/lib/loginViaUsername.ts create mode 100644 ee/apps/account-service/src/lib/removeSession.ts create mode 100644 ee/apps/account-service/src/lib/saveSession.ts rename {apps/meteor/ee/server/services/account => ee/apps/account-service/src}/lib/utils.ts (100%) create mode 100755 ee/apps/account-service/src/service.ts create mode 100644 ee/apps/account-service/tsconfig.json create mode 100644 ee/apps/authorization-service/.eslintrc create mode 100644 ee/apps/authorization-service/Dockerfile create mode 100644 ee/apps/authorization-service/package.json create mode 100755 ee/apps/authorization-service/src/service.ts create mode 100644 ee/apps/authorization-service/tsconfig.json create mode 100644 ee/apps/ddp-streamer/src/proxy.ts delete mode 100644 ee/apps/ddp-streamer/src/streams.ts create mode 100644 ee/apps/stream-hub-service/.eslintrc create mode 100644 ee/apps/stream-hub-service/Dockerfile create mode 100644 ee/apps/stream-hub-service/package.json create mode 100755 ee/apps/stream-hub-service/src/StreamHub.ts create mode 100755 ee/apps/stream-hub-service/src/service.ts create mode 100644 ee/apps/stream-hub-service/tsconfig.json diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 954e211263c..ac1c861bebf 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -156,9 +156,8 @@ jobs: - name: Reset Meteor if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' - run: | - cd ./apps/meteor - meteor reset + working-directory: ./apps/meteor + run: meteor reset - name: Build Rocket.Chat From Pull Request if: startsWith(github.ref, 'refs/pull/') == true @@ -232,6 +231,29 @@ jobs: mongodb-version: ${{ matrix.mongodb-version }} mongodb-replica-set: rs0 + - name: Docker env vars + id: docker-env + run: | + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}" + echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}" + + # test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once) + if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then + RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine" + RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" + else + RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile" + RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" + fi; + + echo "RC_DOCKERFILE: ${RC_DOCKERFILE}" + echo "::set-output name=rc-dockerfile::${RC_DOCKERFILE}" + + echo "RC_DOCKER_TAG: ${RC_DOCKER_TAG}" + echo "::set-output name=rc-docker-tag::${RC_DOCKER_TAG}" + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -264,29 +286,6 @@ jobs: tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz - - name: Docker env vars - id: docker-env - run: | - LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - - echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}" - echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}" - - # test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once) - if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then - RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine" - RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" - else - RC_DOCKERFILE="${{ github.workspace }}/apps/meteor/.docker/Dockerfile" - RC_DOCKER_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" - fi; - - echo "RC_DOCKERFILE: ${RC_DOCKERFILE}" - echo "::set-output name=rc-dockerfile::${RC_DOCKERFILE}" - - echo "RC_DOCKER_TAG: ${RC_DOCKER_TAG}" - echo "::set-output name=rc-docker-tag::${RC_DOCKER_TAG}" - - name: Start containers env: MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' @@ -298,14 +297,6 @@ jobs: run: | docker compose -f docker-compose-ci.yml up -d --build rocketchat - sleep 10 - - until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do - echo "Waiting Rocket.Chat to start up" - ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 - sleep 10 - done - - name: Login to GitHub Container Registry if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' uses: docker/login-action@v2 @@ -333,7 +324,7 @@ jobs: docker push $IMAGE_NAME_BASE fi; - - name: E2E Test API + - name: Wait for Rocket.Chat to start up env: LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} @@ -341,9 +332,21 @@ jobs: DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} run: | docker ps - docker compose -f docker-compose-ci.yml logs rocketchat --tail=50 - cd ./apps/meteor + until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 + sleep 10 + done; + + - name: E2E Test API + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + working-directory: ./apps/meteor + run: | for i in $(seq 1 5); do npm run testapi && s=0 && break || s=$? @@ -376,18 +379,18 @@ jobs: - name: Install Playwright if: steps.cache-playwright.outputs.cache-hit != 'true' - run: | - cd ./apps/meteor - npx playwright install --with-deps + working-directory: ./apps/meteor + run: npx playwright install --with-deps - - name: E2E Test UI + - name: Reset containers env: LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} RC_DOCKERFILE: ${{ steps.docker-env.outputs.rc-dockerfile }} RC_DOCKER_TAG: ${{ steps.docker-env.outputs.rc-docker-tag }} run: | docker ps - docker compose -f docker-compose-ci.yml logs rocketchat --tail=50 + + docker compose -f docker-compose-ci.yml stop rocketchat docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' @@ -399,10 +402,11 @@ jobs: echo "Waiting Rocket.Chat to start up" ((c++)) && ((c==10)) && exit 1 sleep 10 - done + done; - cd ./apps/meteor - yarn test:e2e + - name: E2E Test UI + working-directory: ./apps/meteor + run: yarn test:e2e - name: Store playwright test trace uses: actions/upload-artifact@v2 @@ -444,6 +448,9 @@ jobs: - name: yarn install run: yarn + - name: yarn build + run: yarn build + - name: Unit Test run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' @@ -459,33 +466,26 @@ jobs: tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz + - name: Docker env vars + id: docker-env + run: | + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + echo "LOWERCASE_REPOSITORY: ${LOWERCASE_REPOSITORY}" + echo "::set-output name=lowercase-repo::${LOWERCASE_REPOSITORY}" + - name: Start containers env: MONGO_URL: 'mongodb://host.docker.internal:27017/rocketchat?replicaSet=rs0&directConnection=true' + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official' DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} TRANSPORTER: nats://nats:4222 ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} run: | - export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - docker compose -f docker-compose-ci.yml up -d --build - sleep 10 - - until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do - echo "Waiting 'ddp-streamer' to start up" - ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1 - sleep 10 - done - - until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do - echo "Waiting Rocket.Chat to start up" - ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 - sleep 10 - done - - name: Login to GitHub Container Registry if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' uses: docker/login-action@v2 @@ -498,9 +498,8 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' env: DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} run: | - export LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - docker compose -f docker-compose-ci.yml push \ authorization-service \ account-service \ @@ -508,14 +507,37 @@ jobs: presence-service \ stream-hub-service - - name: E2E Test API + - name: Wait services to start up + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' + RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official' + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} run: | docker ps - docker compose -f docker-compose-ci.yml logs --tail=50 - cd ./apps/meteor + until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'ddp-streamer' to start up" + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1 + sleep 10 + done; + + until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 + sleep 10 + done; + + - name: E2E Test API + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' + RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official' + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + working-directory: ./apps/meteor + run: | for i in $(seq 1 5); do - IS_EE=true npm run testapi && s=0 && break || s=$?; + IS_EE=true npm run testapi && s=0 && break || s=$? docker compose -f ../../docker-compose-ci.yml logs --tail=100 @@ -525,7 +547,7 @@ jobs: NOW=$(date "+%Y-%m-%dT%H:%M:%S.000Z") - docker compose -f ../../docker-compose-ci.yml start + docker compose -f ../../docker-compose-ci.yml restart until echo "$(docker compose -f ../../docker-compose-ci.yml logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do echo "Waiting Rocket.Chat to start up" @@ -535,24 +557,14 @@ jobs: done; exit $s - - name: Cache Playwright binaries - uses: actions/cache@v3 - id: cache-playwright - with: - path: | - ~/.cache/ms-playwright - # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. - key: playwright-1.23.1 - - - name: Install Playwright - run: | - cd ./apps/meteor - npx playwright install --with-deps - - - name: E2E Test UI + - name: Reset containers + env: + LOWERCASE_REPOSITORY: ${{ steps.docker-env.outputs.lowercase-repo }} + RC_DOCKERFILE: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' + RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official' + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} run: | - docker ps - docker compose -f docker-compose-ci.yml logs rocketchat --tail=50 + docker compose -f docker-compose-ci.yml stop docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' @@ -560,15 +572,34 @@ jobs: docker compose -f docker-compose-ci.yml restart - until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do + until echo "$(docker compose -f docker-compose-ci.yml logs ddp-streamer-service)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'ddp-streamer' to start up" + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1 + sleep 10 + done; + + until echo "$(docker compose -f docker-compose-ci.yml logs rocketchat)" | grep -q "SERVER RUNNING"; do echo "Waiting Rocket.Chat to start up" - ((c++)) && ((c==10)) && exit 1 + ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs rocketchat && exit 1 sleep 10 - done + done; + + - name: Cache Playwright binaries + uses: actions/cache@v3 + id: cache-playwright + with: + path: | + ~/.cache/ms-playwright + # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. + key: playwright-1.23.1 - cd ./apps/meteor + - name: Install Playwright + working-directory: ./apps/meteor + run: npx playwright install --with-deps - E2E_COVERAGE=true IS_EE=true yarn test:e2e + - name: E2E Test UI + working-directory: ./apps/meteor + run: E2E_COVERAGE=true IS_EE=true yarn test:e2e - name: Store playwright test trace uses: actions/upload-artifact@v2 @@ -578,9 +609,8 @@ jobs: path: ./apps/meteor/tests/e2e/.playwright* - name: Extract e2e:ee:coverage - run: | - cd ./apps/meteor - yarn test:e2e:nyc + working-directory: ./apps/meteor + run: yarn test:e2e:nyc - uses: codecov/codecov-action@v3 with: @@ -597,7 +627,7 @@ jobs: deploy: runs-on: ubuntu-20.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' - needs: [test, release-versions] + needs: [test, test-ee, release-versions] steps: - uses: actions/checkout@v3 diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 48e6cde45b7..09a69e65db3 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -36,6 +36,7 @@ import { getServicesStatistics } from './getServicesStatistics'; import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; import { Analytics, Team, VideoConf } from '../../../../server/sdk'; import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; +import { isRunningMs } from '../../../../server/lib/isRunningMs'; const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; @@ -342,6 +343,7 @@ export const statistics = { ); const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); + statistics.msEnabled = isRunningMs(); statistics.oplogEnabled = oplogEnabled; statistics.mongoVersion = mongoVersion; statistics.mongoStorageEngine = mongoStorageEngine; diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index 8bc86f0ff4a..8e1e00a27f0 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -125,6 +125,7 @@ export default { lockedAt: '', }, instanceCount: 0, + msEnabled: false, oplogEnabled: false, mongoVersion: '', mongoStorageEngine: '', diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.tsx index 5cd77885c83..65026914591 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.tsx @@ -56,9 +56,9 @@ const DeploymentCard = ({ info, statistics, instances }: DeploymentCardProps): R </Card.Col.Section> <Card.Col.Section> <Card.Col.Title>{t('MongoDB')}</Card.Col.Title> - {`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} (oplog ${ - statistics.oplogEnabled ? t('Enabled') : t('Disabled') - })`} + {`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} ${ + !statistics.msEnabled ? `(oplog ${statistics.oplogEnabled ? t('Enabled') : t('Disabled')})` : '' + }`} </Card.Col.Section> <Card.Col.Section> <Card.Col.Title>{t('Commit_details')}</Card.Col.Title> diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 162aaa75d75..80d72fa2205 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -155,6 +155,7 @@ export default { lockedAt: '', }, instanceCount: 0, + msEnabled: false, oplogEnabled: false, mongoVersion: '', mongoStorageEngine: '', diff --git a/apps/meteor/client/views/admin/info/InformationPage.tsx b/apps/meteor/client/views/admin/info/InformationPage.tsx index 86dc85b3c56..5048fa4732a 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.tsx @@ -33,8 +33,8 @@ const InformationPage = memo(function InformationPage({ return null; } - const usingMultipleInstances = statistics?.instanceCount > 1; - const alertOplogForMultipleInstances = usingMultipleInstances && !statistics.oplogEnabled; + const warningMultipleInstances = !statistics?.msEnabled && statistics?.instanceCount > 1; + const alertOplogForMultipleInstances = warningMultipleInstances && !statistics.oplogEnabled; return ( <Page data-qa='admin-info'> @@ -53,7 +53,9 @@ const InformationPage = memo(function InformationPage({ <Page.ScrollableContentWithShadow> <Box marginBlock='none' marginInline='auto' width='full'> - {usingMultipleInstances && <Callout type='danger' title={t('Multiple_monolith_instances_alert')} marginBlockEnd='x16'></Callout>} + {warningMultipleInstances && ( + <Callout type='danger' title={t('Multiple_monolith_instances_alert')} marginBlockEnd='x16'></Callout> + )} {alertOplogForMultipleInstances && ( <Callout type='danger' diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index cc82c81adcf..ff02d604313 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -103,6 +103,7 @@ export default { lockedAt: '', }, instanceCount: 0, + msEnabled: false, oplogEnabled: false, mongoVersion: '', mongoStorageEngine: '', diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/apps/meteor/ee/server/NetworkBroker.ts index cb6d9c3cf55..f6fb2a1038a 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/apps/meteor/ee/server/NetworkBroker.ts @@ -34,9 +34,6 @@ export class NetworkBroker implements IBroker { this.broker = broker; this.metrics = broker.metrics; - - // TODO move this to a proper startup method? - this.started = this.broker.start(); } async call(method: string, data: any): Promise<any> { @@ -85,11 +82,6 @@ export class NetworkBroker implements IBroker { : Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) ).filter((name) => name !== 'constructor'); - const instanceEvents = instance.getEvents(); - if (!instanceEvents && !methods.length) { - return; - } - const serviceInstance = instance as any; const name = instance.getName(); @@ -97,7 +89,7 @@ export class NetworkBroker implements IBroker { if (!instance.isInternal()) { instance.onEvent('shutdown', async (services) => { if (!services[name]?.includes(this.broker.nodeID)) { - this.broker.logger.debug({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID }); + this.broker.logger.info({ msg: 'Not shutting down, different node.', nodeID: this.broker.nodeID }); return; } this.broker.logger.warn({ msg: 'Received shutdown event, destroying service.', nodeID: this.broker.nodeID }); @@ -105,6 +97,11 @@ export class NetworkBroker implements IBroker { }); } + const instanceEvents = instance.getEvents(); + if (!instanceEvents && !methods.length) { + return; + } + const dependencies = name !== 'license' ? { dependencies: ['license'] } : {}; const service: ServiceSchema = { @@ -185,4 +182,8 @@ export class NetworkBroker implements IBroker { async nodeList(): Promise<IBrokerNode[]> { return this.broker.call('$node.list'); } + + async start(): Promise<void> { + this.started = this.broker.start(); + } } diff --git a/apps/meteor/ee/server/services/account/Account.ts b/apps/meteor/ee/server/services/account/Account.ts deleted file mode 100644 index 4b2ba3ffbba..00000000000 --- a/apps/meteor/ee/server/services/account/Account.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import type { IHashedStampedToken } from './lib/utils'; -import { _generateStampedLoginToken, _hashStampedToken, _hashLoginToken, _tokenExpiration, validatePassword } from './lib/utils'; -import { getCollection, Collections } from '../mongo'; -import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; -import type { IAccount, ILoginResult } from '../../../../server/sdk/types/IAccount'; -import { MeteorError } from '../../../../server/sdk/errors'; - -const saveSession = async (uid: string, newToken: IHashedStampedToken): Promise<void> => { - const Users = await getCollection<IUser>(Collections.User); - await Users.updateOne( - { _id: uid }, - { - $push: { - 'services.resume.loginTokens': newToken.hashedToken, - }, - }, - ); -}; - -const removeSession = async (uid: string, loginToken: string): Promise<void> => { - const Users = await getCollection<IUser>(Collections.User); - await Users.updateOne( - { _id: uid }, - { - $pull: { - 'services.resume.loginTokens': { - $or: [{ hashedToken: loginToken }, { token: loginToken }], - }, - }, - }, - ); -}; - -const loginViaResume = async (resume: string, loginExpiration: number): Promise<false | ILoginResult> => { - const Users = await getCollection<IUser>(Collections.User); - const hashedToken = _hashLoginToken(resume); - - const user = await Users.findOne<IUser>( - { - 'services.resume.loginTokens.hashedToken': hashedToken, - }, - { - projection: { - 'services.resume.loginTokens': 1, - }, - }, - ); - if (!user) { - return false; - } - - const { when } = user.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken) || {}; - - const tokenExpires = when && _tokenExpiration(when, loginExpiration); - if (tokenExpires && new Date() >= tokenExpires) { - throw new MeteorError(403, 'Your session has expired. Please log in again.'); - } - - return { - uid: user._id, - token: resume, - hashedToken, - type: 'resume', - ...(tokenExpires && { tokenExpires }), - }; -}; - -const loginViaUsername = async ( - { username }: { username: string }, - password: string, - loginExpiration: number, -): Promise<false | ILoginResult> => { - const Users = await getCollection<IUser>(Collections.User); - const user = await Users.findOne<IUser>({ username }, { projection: { 'services.password.bcrypt': 1 } }); - if (!user) { - return false; - } - - const valid = user.services?.password?.bcrypt && validatePassword(password, user.services.password.bcrypt); - if (!valid) { - return false; - } - - const newToken = _generateStampedLoginToken(); - - const hashedToken = _hashStampedToken(newToken); - - await saveSession(user._id, hashedToken); - - return { - uid: user._id, - token: newToken.token, - hashedToken: hashedToken.hashedToken, - tokenExpires: _tokenExpiration(newToken.when, loginExpiration), - type: 'password', - }; -}; - -export class Account extends ServiceClass implements IAccount { - protected name = 'accounts'; - - private loginExpiration = 90; - - constructor() { - super(); - - this.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => { - if (clientAction === 'removed') { - return; - } - const { _id, value } = setting; - if (_id !== 'Accounts_LoginExpiration') { - return; - } - if (typeof value === 'number') { - this.loginExpiration = value; - } - }); - } - - async login({ resume, user, password }: { resume: string; user: { username: string }; password: string }): Promise<false | ILoginResult> { - if (resume) { - return loginViaResume(resume, this.loginExpiration); - } - - if (user && password) { - return loginViaUsername(user, password, this.loginExpiration); - } - - return false; - } - - async logout({ userId, token }: { userId: string; token: string }): Promise<void> { - return removeSession(userId, token); - } - - async started(): Promise<void> { - const Settings = await getCollection<any>(Collections.Settings); - - const expiry = await Settings.findOne({ _id: 'Accounts_LoginExpiration' }, { projection: { value: 1 } }); - if (expiry?.value) { - this.loginExpiration = expiry.value; - } - } -} diff --git a/apps/meteor/ee/server/services/account/service.ts b/apps/meteor/ee/server/services/account/service.ts deleted file mode 100644 index aec4f26f0ff..00000000000 --- a/apps/meteor/ee/server/services/account/service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '../../startup/broker'; - -import { api } from '../../../../server/sdk/api'; -import { Account } from './Account'; - -api.registerService(new Account()); diff --git a/apps/meteor/ee/server/services/authorization/service.ts b/apps/meteor/ee/server/services/authorization/service.ts deleted file mode 100644 index d2a3a877a55..00000000000 --- a/apps/meteor/ee/server/services/authorization/service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Document } from 'mongodb'; - -import '../../startup/broker'; - -import { api } from '../../../../server/sdk/api'; -import { Authorization } from '../../../../server/services/authorization/service'; -import { Collections, getCollection, getConnection } from '../mongo'; -import { registerServiceModels } from '../../lib/registerServiceModels'; - -getConnection().then(async (db) => { - const trash = await getCollection<Document>(Collections.Trash); - - registerServiceModels(db, trash); - - api.registerService(new Authorization(db)); -}); diff --git a/apps/meteor/ee/server/services/ecosystem.config.js b/apps/meteor/ee/server/services/ecosystem.config.js deleted file mode 100644 index 1b4190cebb4..00000000000 --- a/apps/meteor/ee/server/services/ecosystem.config.js +++ /dev/null @@ -1,33 +0,0 @@ -const watch = ['.', '../broker.ts', '../../../server/sdk']; - -module.exports = { - apps: [ - { - name: 'authorization', - watch: [...watch, '../../../server/services/authorization'], - }, - // { - // name: 'presence', - // }, - { - name: 'account', - }, - { - name: 'stream-hub', - }, - // { - // name: 'ddp-streamer', - // }, - ].map((app) => - Object.assign(app, { - script: app.script || `ts-node --files ${app.name}/service.ts`, - watch: app.watch || ['.', '../broker.ts', '../../../server/sdk', '../../../server/modules'], - instances: 1, - env: { - MOLECULER_LOG_LEVEL: 'info', - TRANSPORTER: 'nats://localhost:4222', - MONGO_URL: 'mongodb://localhost:3001/meteor', - }, - }), - ), -}; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 78acd594366..277798c838e 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -7,10 +7,6 @@ "scripts": { "dev": "pm2 start ecosystem.config.js", "pm2": "pm2", - "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} run-p start:account start:authorization start:stream-hub", - "start:account": "ts-node --files ./account/service.ts", - "start:authorization": "ts-node --files ./authorization/service.ts", - "start:stream-hub": "ts-node --files ./stream-hub/service.ts", "typecheck": "tsc --noEmit --skipLibCheck", "build": "tsc", "build-containers": "npm run build && docker-compose build && rm -rf ./dist", diff --git a/apps/meteor/ee/server/services/stream-hub/StreamHub.ts b/apps/meteor/ee/server/services/stream-hub/StreamHub.ts deleted file mode 100755 index 99d2f076adf..00000000000 --- a/apps/meteor/ee/server/services/stream-hub/StreamHub.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IServiceClass } from '../../../../server/sdk/types/ServiceClass'; -import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; -import { getConnection } from '../mongo'; -import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; -import { api } from '../../../../server/sdk/api'; -import { DatabaseWatcher } from '../../../../server/database/DatabaseWatcher'; - -export class StreamHub extends ServiceClass implements IServiceClass { - protected name = 'hub'; - - async created(): Promise<void> { - const db = await getConnection({ maxPoolSize: 1 }); - - const watcher = new DatabaseWatcher({ db }); - - initWatchers(watcher, api.broadcast.bind(api)); - - watcher.watch(); - } -} diff --git a/apps/meteor/ee/server/services/stream-hub/service.ts b/apps/meteor/ee/server/services/stream-hub/service.ts deleted file mode 100755 index 1faba503531..00000000000 --- a/apps/meteor/ee/server/services/stream-hub/service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Document } from 'mongodb'; - -import '../../startup/broker'; - -import { api } from '../../../../server/sdk/api'; -import { Collections, getCollection, getConnection } from '../mongo'; -import { registerServiceModels } from '../../lib/registerServiceModels'; - -getConnection().then(async (db) => { - const trash = await getCollection<Document>(Collections.Trash); - - registerServiceModels(db, trash); - - // need to import StreamHub service after models are registered - const { StreamHub } = await import('./StreamHub'); - - api.registerService(new StreamHub()); -}); diff --git a/apps/meteor/ee/server/services/tsconfig.json b/apps/meteor/ee/server/services/tsconfig.json index e718d161dab..4c706287782 100644 --- a/apps/meteor/ee/server/services/tsconfig.json +++ b/apps/meteor/ee/server/services/tsconfig.json @@ -43,6 +43,6 @@ // "emitDecoratorMetadata": true, // "experimentalDecorators": true, }, - "include": ["./**/*", "../../../definition/externals/meteor/rocketchat-streamer.d.ts"], + "include": ["./**/*", "../../../definition/externals/meteor/rocketchat-streamer.d.ts", "../../../../../ee/apps/account-service/src/lib"], "exclude": ["./dist", "./ecosystem.config.js", "../../../definition/methods"] } diff --git a/apps/meteor/ee/server/startup/broker.ts b/apps/meteor/ee/server/startup/broker.ts index 48e5330b7f1..e435d143cec 100644 --- a/apps/meteor/ee/server/startup/broker.ts +++ b/apps/meteor/ee/server/startup/broker.ts @@ -2,7 +2,6 @@ import EJSON from 'ejson'; import { Errors, Serializers, ServiceBroker } from 'moleculer'; import { pino } from 'pino'; -import { api } from '../../../server/sdk/api'; import { isMeteorError, MeteorError } from '../../../server/sdk/errors'; import { NetworkBroker } from '../NetworkBroker'; @@ -171,4 +170,4 @@ const network = new ServiceBroker({ }, }); -api.setBroker(new NetworkBroker(network)); +export const broker = new NetworkBroker(network); diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index 4b2b8afd0ea..2cbfa70443d 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -3,9 +3,15 @@ import './engagementDashboard'; import './seatsCap'; import './services'; import './upsell'; +import { api } from '../../../server/sdk/api'; import { isRunningMs } from '../../../server/lib/isRunningMs'; // only starts network broker if running in micro services mode if (isRunningMs()) { - require('./broker'); + (async () => { + const { broker } = await import('./broker'); + + api.setBroker(broker); + api.start(); + })(); } diff --git a/apps/meteor/server/sdk/api.ts b/apps/meteor/server/sdk/api.ts index 4d7df7db62c..e99fc6bfcc1 100644 --- a/apps/meteor/server/sdk/api.ts +++ b/apps/meteor/server/sdk/api.ts @@ -6,4 +6,5 @@ export const api = new Api(); if (!isRunningMs()) { api.setBroker(new LocalBroker()); + api.start(); } diff --git a/apps/meteor/server/sdk/lib/Api.ts b/apps/meteor/server/sdk/lib/Api.ts index be2f6729648..f16bdd18a03 100644 --- a/apps/meteor/server/sdk/lib/Api.ts +++ b/apps/meteor/server/sdk/lib/Api.ts @@ -64,4 +64,11 @@ export class Api implements IApiService { nodeList(): Promise<IBrokerNode[]> { return this.broker.nodeList(); } + + async start(): Promise<void> { + if (!this.broker) { + throw new Error('No broker set to start.'); + } + await this.broker.start(); + } } diff --git a/apps/meteor/server/sdk/lib/LocalBroker.ts b/apps/meteor/server/sdk/lib/LocalBroker.ts index a63490d4cf8..d7205c72cae 100644 --- a/apps/meteor/server/sdk/lib/LocalBroker.ts +++ b/apps/meteor/server/sdk/lib/LocalBroker.ts @@ -13,6 +13,8 @@ export class LocalBroker implements IBroker { private events = new EventEmitter(); + private services = new Set<IServiceClass>(); + async call(method: string, data: any): Promise<any> { const result = await asyncLocalStorage.run( { @@ -54,6 +56,8 @@ export class LocalBroker implements IBroker { createService(instance: IServiceClass): void { const namespace = instance.getName(); + this.services.add(instance); + instance.created(); instance.getEvents().forEach((eventName) => { @@ -74,8 +78,6 @@ export class LocalBroker implements IBroker { this.methods.set(`${namespace}.${method}`, i[method].bind(i)); } - - instance.started(); } async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> { @@ -102,4 +104,8 @@ export class LocalBroker implements IBroker { return instances.map(({ _id }) => ({ id: _id, available: true })); } + + async start(): Promise<void> { + await Promise.all([...this.services].map((service) => service.started())); + } } diff --git a/apps/meteor/server/sdk/types/IBroker.ts b/apps/meteor/server/sdk/types/IBroker.ts index 910432cd52e..a56d4fa8ba3 100644 --- a/apps/meteor/server/sdk/types/IBroker.ts +++ b/apps/meteor/server/sdk/types/IBroker.ts @@ -59,4 +59,5 @@ export interface IBroker { broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>; broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>; nodeList(): Promise<IBrokerNode[]>; + start(): Promise<void>; } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 00d140377f1..57b762a8d1d 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -1,7 +1,6 @@ -import type { Db, Collection } from 'mongodb'; import mem from 'mem'; import type { IUser, IRole, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms, Users, Roles } from '@rocket.chat/models'; +import { Subscriptions, Rooms, Users, Roles, Permissions } from '@rocket.chat/models'; import type { IAuthorization, RoomAccessValidator } from '../../sdk/types/IAuthorization'; import { ServiceClass } from '../../sdk/types/ServiceClass'; @@ -15,8 +14,6 @@ import './canAccessRoomLivechat'; export class Authorization extends ServiceClass implements IAuthorization { protected name = 'authorization'; - private Permissions: Collection; - private getRolesCached = mem(this.getRoles.bind(this), { maxAge: 1000, cacheKey: JSON.stringify, @@ -27,11 +24,9 @@ export class Authorization extends ServiceClass implements IAuthorization { ...(process.env.TEST_MODE === 'true' && { maxAge: 1 }), }); - constructor(db: Db) { + constructor() { super(); - this.Permissions = db.collection('rocketchat_permissions'); - const clearCache = (): void => { mem.clear(this.getRolesCached); mem.clear(this.rolesHasPermissionCached); @@ -148,7 +143,7 @@ export class Authorization extends ServiceClass implements IAuthorization { return false; } - const result = await this.Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } }); + const result = await Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } }); return !!result; } diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 6f389cf593d..55b7770b0e7 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -50,6 +50,6 @@ if (!isRunningMs()) { const { Authorization } = await import('./authorization/service'); api.registerService(new Presence()); - api.registerService(new Authorization(db)); + api.registerService(new Authorization()); })(); } diff --git a/apps/meteor/server/startup/serverRunning.js b/apps/meteor/server/startup/serverRunning.js index 1cd991f7c5e..b4276a474a3 100644 --- a/apps/meteor/server/startup/serverRunning.js +++ b/apps/meteor/server/startup/serverRunning.js @@ -29,6 +29,8 @@ Meteor.startup(function () { const desiredNodeVersionMajor = String(semver.parse(desiredNodeVersion).major); return Meteor.setTimeout(function () { + const replicaSet = isRunningMs() ? 'Not required (running micro services)' : `${oplogEnabled ? 'Enabled' : 'Disabled'}`; + let msg = [ `Rocket.Chat Version: ${Info.version}`, ` NodeJS Version: ${process.versions.node} - ${process.arch}`, @@ -37,7 +39,7 @@ Meteor.startup(function () { ` Platform: ${process.platform}`, ` Process Port: ${process.env.PORT}`, ` Site URL: ${settings.get('Site_Url')}`, - ` ReplicaSet OpLog: ${oplogEnabled ? 'Enabled' : 'Disabled'}`, + ` ReplicaSet OpLog: ${replicaSet}`, ]; if (Info.commit && Info.commit.hash) { diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 3444c4300fa..c1c356683b0 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -26,9 +26,9 @@ services: authorization-service: build: - dockerfile: apps/meteor/ee/server/services/Dockerfile + dockerfile: ee/apps/authorization-service/Dockerfile args: - SERVICE: authorization + SERVICE: authorization-service image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG} environment: - 'MONGO_URL=${MONGO_URL}' @@ -41,9 +41,9 @@ services: account-service: build: - dockerfile: apps/meteor/ee/server/services/Dockerfile + dockerfile: ee/apps/account-service/Dockerfile args: - SERVICE: account + SERVICE: account-service image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG} environment: - MONGO_URL=${MONGO_URL} @@ -92,9 +92,9 @@ services: stream-hub-service: build: - dockerfile: apps/meteor/ee/server/services/Dockerfile + dockerfile: ee/apps/stream-hub-service/Dockerfile args: - SERVICE: stream-hub + SERVICE: stream-hub-service image: ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${DOCKER_TAG} environment: - MONGO_URL=${MONGO_URL} @@ -107,10 +107,6 @@ services: nats: image: nats:2.6-alpine - extra_hosts: - - 'host.docker.internal:host-gateway' - ports: - - '4222:4222' traefik: image: traefik:v2.8 diff --git a/ee/apps/account-service/.eslintrc b/ee/apps/account-service/.eslintrc new file mode 100644 index 00000000000..4d3f4a7d4d5 --- /dev/null +++ b/ee/apps/account-service/.eslintrc @@ -0,0 +1,16 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"], + "plugins": ["jest"], + "env": { + "jest/globals": true + } +} diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile new file mode 100644 index 00000000000..c27bf31f9e5 --- /dev/null +++ b/ee/apps/account-service/Dockerfile @@ -0,0 +1,34 @@ +FROM node:14.19.3-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json new file mode 100644 index 00000000000..b5650aa8018 --- /dev/null +++ b/ee/apps/account-service/package.json @@ -0,0 +1,50 @@ +{ + "name": "@rocket.chat/account-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat Account service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "next", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/string-helpers": "next", + "@types/node": "^14.18.21", + "bcrypt": "^5.0.1", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "mongodb": "^4.3.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/bcrypt": "^5.0.0", + "@types/eslint": "^8", + "@types/polka": "^0.5.4", + "eslint": "^8.21.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/account-service/src/service.js", + "files": [ + "/dist" + ] +} diff --git a/ee/apps/account-service/src/Account.ts b/ee/apps/account-service/src/Account.ts new file mode 100644 index 00000000000..34dce6271dc --- /dev/null +++ b/ee/apps/account-service/src/Account.ts @@ -0,0 +1,53 @@ +import { Settings } from '@rocket.chat/models'; + +import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; +import type { IAccount, ILoginResult } from '../../../../apps/meteor/server/sdk/types/IAccount'; +import { removeSession } from './lib/removeSession'; +import { loginViaResume } from './lib/loginViaResume'; +import { loginViaUsername } from './lib/loginViaUsername'; + +export class Account extends ServiceClass implements IAccount { + protected name = 'accounts'; + + private loginExpiration = 90; + + constructor() { + super(); + + this.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => { + if (clientAction === 'removed') { + return; + } + const { _id, value } = setting; + if (_id !== 'Accounts_LoginExpiration') { + return; + } + if (typeof value === 'number') { + this.loginExpiration = value; + } + }); + } + + async login({ resume, user, password }: { resume: string; user: { username: string }; password: string }): Promise<false | ILoginResult> { + if (resume) { + return loginViaResume(resume, this.loginExpiration); + } + + if (user && password) { + return loginViaUsername(user, password, this.loginExpiration); + } + + return false; + } + + async logout({ userId, token }: { userId: string; token: string }): Promise<void> { + return removeSession(userId, token); + } + + async started(): Promise<void> { + const expiry = await Settings.findOne({ _id: 'Accounts_LoginExpiration' }, { projection: { value: 1 } }); + if (expiry?.value) { + this.loginExpiration = expiry.value as number; + } + } +} diff --git a/ee/apps/account-service/src/lib/loginViaResume.ts b/ee/apps/account-service/src/lib/loginViaResume.ts new file mode 100644 index 00000000000..265f5779726 --- /dev/null +++ b/ee/apps/account-service/src/lib/loginViaResume.ts @@ -0,0 +1,39 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { _hashLoginToken, _tokenExpiration } from './utils'; +import type { ILoginResult } from '../../../../../apps/meteor/server/sdk/types/IAccount'; +import { MeteorError } from '../../../../../apps/meteor/server/sdk/errors'; + +export async function loginViaResume(resume: string, loginExpiration: number): Promise<false | ILoginResult> { + const hashedToken = _hashLoginToken(resume); + + const user = await Users.findOne<IUser>( + { + 'services.resume.loginTokens.hashedToken': hashedToken, + }, + { + projection: { + 'services.resume.loginTokens': 1, + }, + }, + ); + if (!user) { + return false; + } + + const { when } = user.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken) || {}; + + const tokenExpires = when && _tokenExpiration(when, loginExpiration); + if (tokenExpires && new Date() >= tokenExpires) { + throw new MeteorError(403, 'Your session has expired. Please log in again.'); + } + + return { + uid: user._id, + token: resume, + hashedToken, + type: 'resume', + ...(tokenExpires && { tokenExpires }), + }; +} diff --git a/ee/apps/account-service/src/lib/loginViaUsername.ts b/ee/apps/account-service/src/lib/loginViaUsername.ts new file mode 100644 index 00000000000..c5f7cd6afbc --- /dev/null +++ b/ee/apps/account-service/src/lib/loginViaUsername.ts @@ -0,0 +1,36 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { _generateStampedLoginToken, _hashStampedToken, _tokenExpiration, validatePassword } from './utils'; +import type { ILoginResult } from '../../../../../apps/meteor/server/sdk/types/IAccount'; +import { saveSession } from './saveSession'; + +export async function loginViaUsername( + { username }: { username: string }, + password: string, + loginExpiration: number, +): Promise<false | ILoginResult> { + const user = await Users.findOne<IUser>({ username }, { projection: { 'services.password.bcrypt': 1 } }); + if (!user) { + return false; + } + + const valid = user.services?.password?.bcrypt && validatePassword(password, user.services.password.bcrypt); + if (!valid) { + return false; + } + + const newToken = _generateStampedLoginToken(); + + const hashedToken = _hashStampedToken(newToken); + + await saveSession(user._id, hashedToken); + + return { + uid: user._id, + token: newToken.token, + hashedToken: hashedToken.hashedToken, + tokenExpires: _tokenExpiration(newToken.when, loginExpiration), + type: 'password', + }; +} diff --git a/ee/apps/account-service/src/lib/removeSession.ts b/ee/apps/account-service/src/lib/removeSession.ts new file mode 100644 index 00000000000..8f402e49542 --- /dev/null +++ b/ee/apps/account-service/src/lib/removeSession.ts @@ -0,0 +1,14 @@ +import { Users } from '@rocket.chat/models'; + +export async function removeSession(uid: string, loginToken: string): Promise<void> { + await Users.updateOne( + { _id: uid }, + { + $pull: { + 'services.resume.loginTokens': { + $or: [{ hashedToken: loginToken }, { token: loginToken }], + }, + }, + }, + ); +} diff --git a/ee/apps/account-service/src/lib/saveSession.ts b/ee/apps/account-service/src/lib/saveSession.ts new file mode 100644 index 00000000000..07dc50ee417 --- /dev/null +++ b/ee/apps/account-service/src/lib/saveSession.ts @@ -0,0 +1,14 @@ +import { Users } from '@rocket.chat/models'; + +import type { IHashedStampedToken } from './utils'; + +export async function saveSession(uid: string, newToken: IHashedStampedToken): Promise<void> { + await Users.updateOne( + { _id: uid }, + { + $push: { + 'services.resume.loginTokens': newToken.hashedToken, + }, + }, + ); +} diff --git a/apps/meteor/ee/server/services/account/lib/utils.ts b/ee/apps/account-service/src/lib/utils.ts similarity index 100% rename from apps/meteor/ee/server/services/account/lib/utils.ts rename to ee/apps/account-service/src/lib/utils.ts diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts new file mode 100755 index 00000000000..91c62c199d2 --- /dev/null +++ b/ee/apps/account-service/src/service.ts @@ -0,0 +1,33 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; + +import { api } from '../../../../apps/meteor/server/sdk/api'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3033; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection<Document>(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { Account } = await import('./Account'); + + api.registerService(new Account()); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + await api.nodeList(); + res.end('ok'); + }) + .listen(PORT); +})(); diff --git a/ee/apps/account-service/tsconfig.json b/ee/apps/account-service/tsconfig.json new file mode 100644 index 00000000000..fd62af76f07 --- /dev/null +++ b/ee/apps/account-service/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/apps/authorization-service/.eslintrc b/ee/apps/authorization-service/.eslintrc new file mode 100644 index 00000000000..4d3f4a7d4d5 --- /dev/null +++ b/ee/apps/authorization-service/.eslintrc @@ -0,0 +1,16 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"], + "plugins": ["jest"], + "env": { + "jest/globals": true + } +} diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile new file mode 100644 index 00000000000..c27bf31f9e5 --- /dev/null +++ b/ee/apps/authorization-service/Dockerfile @@ -0,0 +1,34 @@ +FROM node:14.19.3-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json new file mode 100644 index 00000000000..824768de3cf --- /dev/null +++ b/ee/apps/authorization-service/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rocket.chat/authorization-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat Authorization service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "next", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/string-helpers": "next", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "mongodb": "^4.3.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/eslint": "^8", + "@types/polka": "^0.5.4", + "eslint": "^8.21.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/authorization-service/src/service.js", + "files": [ + "/dist" + ] +} diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts new file mode 100755 index 00000000000..269fa278e6c --- /dev/null +++ b/ee/apps/authorization-service/src/service.ts @@ -0,0 +1,33 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; + +import { api } from '../../../../apps/meteor/server/sdk/api'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3034; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection<Document>(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { Authorization } = await import('../../../../apps/meteor/server/services/authorization/service'); + + api.registerService(new Authorization()); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + await api.nodeList(); + res.end('ok'); + }) + .listen(PORT); +})(); diff --git a/ee/apps/authorization-service/tsconfig.json b/ee/apps/authorization-service/tsconfig.json new file mode 100644 index 00000000000..fd62af76f07 --- /dev/null +++ b/ee/apps/authorization-service/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 487af3329c6..c27bf31f9e5 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -12,8 +12,6 @@ COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist -COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json -COPY ./packages/ui-contexts/dist packages/ui-contexts/dist COPY ./ee/apps/${SERVICE}/dist . diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index b315ac489c9..5f429eed551 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -22,16 +22,16 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", - "@rocket.chat/ui-contexts": "workspace:^", "colorette": "^1.4.0", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", - "fibers": "^5.0.1", + "fibers": "^5.0.3", "jaeger-client": "^3.19.0", "moleculer": "^0.14.21", "mongodb": "^4.3.1", "nats": "^2.4.0", "pino": "^7.11.0", + "polka": "^0.5.2", "sharp": "^0.30.7", "underscore": "^1.13.4", "uuid": "^7.0.3", @@ -43,6 +43,7 @@ "@types/eslint": "^8", "@types/meteor": "2.7.1", "@types/node": "^14.18.21", + "@types/polka": "^0.5.4", "@types/sharp": "^0.30.4", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3", diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index f0c873e90b7..dcac442f720 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -1,68 +1,29 @@ -import type { IncomingMessage, RequestOptions, ServerResponse } from 'http'; -import http from 'http'; -import url from 'url'; +import crypto from 'crypto'; +import polka from 'polka'; import WebSocket from 'ws'; +import type { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module'; import { ListenersModule } from '../../../../apps/meteor/server/modules/listeners/listeners.module'; import { StreamerCentral } from '../../../../apps/meteor/server/modules/streamer/streamer.module'; import { MeteorService, Presence } from '../../../../apps/meteor/server/sdk'; import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; -import { api } from '../../../../apps/meteor/server/sdk/api'; import { Client } from './Client'; import { events, server } from './configureServer'; import { DDP_EVENTS } from './constants'; import { Autoupdate } from './lib/Autoupdate'; -import { notifications } from './streams'; - -const { PORT: port = 4000 } = process.env; - -const proxy = function (req: IncomingMessage, res: ServerResponse): void { - req.pause(); - const options: RequestOptions = url.parse(req.url || ''); - options.headers = req.headers; - options.method = req.method; - options.agent = false; - options.hostname = 'localhost'; - options.port = 3000; - - const connector = http.request(options, function (serverResponse) { - serverResponse.pause(); - if (serverResponse.statusCode) { - res.writeHead(serverResponse.statusCode, serverResponse.headers); - } - serverResponse.pipe(res); - serverResponse.resume(); - }); - req.pipe(connector); - req.resume(); -}; - -const httpServer = http.createServer((req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - - if (process.env.NODE_ENV !== 'production' && !/^\/sockjs\/info\?cb=/.test(req.url || '')) { - return proxy(req, res); - - // res.writeHead(404); - // return res.end(); - } +import { proxy } from './proxy'; - res.writeHead(200, { 'Content-Type': 'text/plain' }); - - res.end('{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":666}'); -}); - -httpServer.listen(port); - -const wss = new WebSocket.Server({ server: httpServer }); - -wss.on('connection', (ws, req) => new Client(ws, req.url !== '/websocket', req)); +const { PORT = 4000 } = process.env; export class DDPStreamer extends ServiceClass { protected name = 'streamer'; - constructor() { + private app?: polka.Polka; + + private wss?: WebSocket.Server; + + constructor(notifications: NotificationsModule) { super(); new ListenersModule(this, notifications); @@ -90,15 +51,6 @@ export class DDPStreamer extends ServiceClass { }); } - async started(): Promise<void> { - // TODO this call creates a dependency to MeteorService, should it be a hard dependency? or can this call fail and be ignored? - const versions = await MeteorService.getAutoUpdateClientVersions(); - - Object.keys(versions).forEach((key) => { - Autoupdate.updateVersion(versions[key]); - }); - } - async created(): Promise<void> { if (!this.context) { return; @@ -147,13 +99,13 @@ export class DDPStreamer extends ServiceClass { const { userId, connection } = info; Presence.newConnection(userId, connection.id, nodeID); - api.broadcast('accounts.login', { userId, connection }); + this.api.broadcast('accounts.login', { userId, connection }); }); server.on(DDP_EVENTS.LOGGEDOUT, (info) => { const { userId, connection } = info; - api.broadcast('accounts.logout', { userId, connection }); + this.api.broadcast('accounts.logout', { userId, connection }); if (!userId) { return; @@ -164,7 +116,7 @@ export class DDPStreamer extends ServiceClass { server.on(DDP_EVENTS.DISCONNECTED, (info) => { const { userId, connection } = info; - api.broadcast('socket.disconnected', connection); + this.api.broadcast('socket.disconnected', connection); if (!userId) { return; @@ -173,7 +125,45 @@ export class DDPStreamer extends ServiceClass { }); server.on(DDP_EVENTS.CONNECTED, ({ connection }) => { - api.broadcast('socket.connected', connection); + this.api.broadcast('socket.connected', connection); + }); + } + + async started(): Promise<void> { + // TODO this call creates a dependency to MeteorService, should it be a hard dependency? or can this call fail and be ignored? + const versions = await MeteorService.getAutoUpdateClientVersions(); + + Object.keys(versions).forEach((key) => { + Autoupdate.updateVersion(versions[key]); }); + + this.app = polka() + .use(proxy()) + .get('/health', async (_req, res) => { + await this.api.nodeList(); + res.end('ok'); + }) + .get('*', function (_req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Content-Type', 'application/json'); + + res.writeHead(200); + + res.end(`{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":${crypto.randomBytes(4).readUInt32LE(0)},"ms":true}`); + }) + .listen(PORT); + + this.wss = new WebSocket.Server({ server: this.app.server }); + + this.wss.on('connection', (ws, req) => new Client(ws, req.url !== '/websocket', req)); + } + + async stopped(): Promise<void> { + this.wss?.clients.forEach(function (client) { + client.terminate(); + }); + + this.app?.server?.close(); + this.wss?.close(); } } diff --git a/ee/apps/ddp-streamer/src/proxy.ts b/ee/apps/ddp-streamer/src/proxy.ts new file mode 100644 index 00000000000..1243a324145 --- /dev/null +++ b/ee/apps/ddp-streamer/src/proxy.ts @@ -0,0 +1,41 @@ +import type { IncomingMessage, RequestOptions, ServerResponse } from 'http'; +import http from 'http'; +import url from 'url'; + +import type polka from 'polka'; + +const isProdEnv = process.env.NODE_ENV === 'production'; + +const skipProxyPaths = [/^\/sockjs\/info\?cb=/, /^\/health/]; + +export function proxy(): (req: IncomingMessage, res: ServerResponse, next: polka.Next) => void { + if (isProdEnv) { + return (_req, _res, next) => next(); + } + + return (req, res, next) => { + if (skipProxyPaths.some((regex) => regex.test(req.url || ''))) { + return next(); + } + + req.pause(); + + const options: RequestOptions = url.parse(req.url || ''); + options.headers = req.headers; + options.method = req.method; + options.agent = false; + options.hostname = 'localhost'; + options.port = 3000; + + const connector = http.request(options, function (serverResponse) { + serverResponse.pause(); + if (serverResponse.statusCode) { + res.writeHead(serverResponse.statusCode, serverResponse.headers); + } + serverResponse.pipe(res); + serverResponse.resume(); + }); + req.pipe(connector); + req.resume(); + }; +} diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index 6f4fcfc0fdb..9b4bc463192 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,6 +1,29 @@ -import '../../../../apps/meteor/ee/server/startup/broker'; +import type { Document } from 'mongodb'; import { api } from '../../../../apps/meteor/server/sdk/api'; -import { DDPStreamer } from './DDPStreamer'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -api.registerService(new DDPStreamer()); +(async () => { + const db = await getConnection(); + + const trash = await getCollection<Document>(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { NotificationsModule } = await import('../../../../apps/meteor/server/modules/notifications/notifications.module'); + const { DDPStreamer } = await import('./DDPStreamer'); + const { Stream } = await import('./Streamer'); + + const notifications = new NotificationsModule(Stream); + + notifications.configure(); + + api.registerService(new DDPStreamer(notifications)); + + await api.start(); +})(); diff --git a/ee/apps/ddp-streamer/src/streams.ts b/ee/apps/ddp-streamer/src/streams.ts deleted file mode 100644 index e567bd5bf86..00000000000 --- a/ee/apps/ddp-streamer/src/streams.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Document } from 'mongodb'; - -import { NotificationsModule } from '../../../../apps/meteor/server/modules/notifications/notifications.module'; -import { Stream } from './Streamer'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; - -export const notifications = new NotificationsModule(Stream); - -getConnection().then(async (db) => { - const trash = await getCollection<Document>(Collections.Trash); - - registerServiceModels(db, trash); - - notifications.configure(); -}); diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index f99c3b09fa2..c49bba3bc1c 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -15,13 +15,16 @@ ], "author": "Rocket.Chat", "dependencies": { + "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "next", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "next", "@types/node": "^14.18.21", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", - "fibers": "^5.0.1", + "fibers": "^5.0.3", "moleculer": "^0.14.21", "mongodb": "^4.3.1", "nats": "^2.4.0", diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index e0cec05d441..fe7bc5b86b0 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,28 +1,33 @@ import type { Document } from 'mongodb'; import polka from 'polka'; -import '../../../../apps/meteor/ee/server/startup/broker'; - import { api } from '../../../../apps/meteor/server/sdk/api'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; const PORT = process.env.PORT || 3031; -getConnection().then(async (db) => { +(async () => { + const db = await getConnection(); + const trash = await getCollection<Document>(Collections.Trash); registerServiceModels(db, trash); + api.setBroker(broker); + // need to import Presence service after models are registered const { Presence } = await import('@rocket.chat/presence'); api.registerService(new Presence()); + await api.start(); + polka() .get('/health', async function (_req, res) { await api.nodeList(); res.end('ok'); }) .listen(PORT); -}); +})(); diff --git a/ee/apps/presence-service/tsconfig.json b/ee/apps/presence-service/tsconfig.json index 8893cea8ceb..fd62af76f07 100644 --- a/ee/apps/presence-service/tsconfig.json +++ b/ee/apps/presence-service/tsconfig.json @@ -26,6 +26,6 @@ "declarationMap": false }, "files": ["./src/service.ts"], - "include": ["../../../apps/meteor/definition"], + "include": ["../../../apps/meteor/definition/externals/meteor"], "exclude": ["./dist"] } diff --git a/ee/apps/stream-hub-service/.eslintrc b/ee/apps/stream-hub-service/.eslintrc new file mode 100644 index 00000000000..4d3f4a7d4d5 --- /dev/null +++ b/ee/apps/stream-hub-service/.eslintrc @@ -0,0 +1,16 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.spec.jsx"], + "env": { + "jest": true + } + } + ], + "ignorePatterns": ["**/dist"], + "plugins": ["jest"], + "env": { + "jest/globals": true + } +} diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile new file mode 100644 index 00000000000..c27bf31f9e5 --- /dev/null +++ b/ee/apps/stream-hub-service/Dockerfile @@ -0,0 +1,34 @@ +FROM node:14.19.3-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json new file mode 100644 index 00000000000..6634c1d8dc6 --- /dev/null +++ b/ee/apps/stream-hub-service/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rocket.chat/stream-hub-service", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat Stream Hub service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "next", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/string-helpers": "next", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "mongodb": "^4.3.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/bcrypt": "^5.0.0", + "@types/eslint": "^8", + "@types/polka": "^0.5.4", + "eslint": "^8.21.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/stream-hub-service/src/service.js", + "files": [ + "/dist" + ] +} diff --git a/ee/apps/stream-hub-service/src/StreamHub.ts b/ee/apps/stream-hub-service/src/StreamHub.ts new file mode 100755 index 00000000000..d18df7484f3 --- /dev/null +++ b/ee/apps/stream-hub-service/src/StreamHub.ts @@ -0,0 +1,22 @@ +import type { Db } from 'mongodb'; + +import type { IServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; +import { ServiceClass } from '../../../../apps/meteor/server/sdk/types/ServiceClass'; +import { initWatchers } from '../../../../apps/meteor/server/modules/watchers/watchers.module'; +import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; + +export class StreamHub extends ServiceClass implements IServiceClass { + protected name = 'hub'; + + constructor(private db: Db) { + super(); + } + + async created(): Promise<void> { + const watcher = new DatabaseWatcher({ db: this.db }); + + initWatchers(watcher, this.api.broadcast.bind(this.api)); + + watcher.watch(); + } +} diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts new file mode 100755 index 00000000000..45842b60970 --- /dev/null +++ b/ee/apps/stream-hub-service/src/service.ts @@ -0,0 +1,33 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; + +import { api } from '../../../../apps/meteor/server/sdk/api'; +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; + +const PORT = process.env.PORT || 3035; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection<Document>(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { StreamHub } = await import('./StreamHub'); + + api.registerService(new StreamHub(db)); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + await api.nodeList(); + res.end('ok'); + }) + .listen(PORT); +})(); diff --git a/ee/apps/stream-hub-service/tsconfig.json b/ee/apps/stream-hub-service/tsconfig.json new file mode 100644 index 00000000000..fd62af76f07 --- /dev/null +++ b/ee/apps/stream-hub-service/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "es2018", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "incremental": true, + + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "strictFunctionTypes": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "outDir": "./dist", + "importsNotUsedAsValues": "preserve", + "declaration": false, + "declarationMap": false + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 528681a381a..67f0c4406c8 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -76,6 +76,7 @@ export interface IStats { }; instanceCount: number; oplogEnabled: boolean; + msEnabled: boolean; mongoVersion: string; mongoStorageEngine: string; pushQueue: number; diff --git a/packages/presence/package.json b/packages/presence/package.json index 7f98deef502..3e506bde2de 100644 --- a/packages/presence/package.json +++ b/packages/presence/package.json @@ -9,7 +9,6 @@ "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/ui-contexts": "workspace:^", "@types/node": "^14.18.21", "babel-jest": "^29.0.3", "eslint": "^8.21.0", diff --git a/yarn.lock b/yarn.lock index bcd37ca9f06..61d4c6be812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5462,6 +5462,38 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/account-service@workspace:ee/apps/account-service": + version: 0.0.0-use.local + resolution: "@rocket.chat/account-service@workspace:ee/apps/account-service" + dependencies: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": next + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/string-helpers": next + "@types/bcrypt": ^5.0.0 + "@types/eslint": ^8 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + bcrypt: ^5.0.1 + ejson: ^2.2.2 + eslint: ^8.21.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + mongodb: ^4.3.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + uuid: ^9.0.0 + languageName: unknown + linkType: soft + "@rocket.chat/agenda@workspace:^, @rocket.chat/agenda@workspace:packages/agenda": version: 0.0.0-use.local resolution: "@rocket.chat/agenda@workspace:packages/agenda" @@ -5542,6 +5574,35 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/authorization-service@workspace:ee/apps/authorization-service": + version: 0.0.0-use.local + resolution: "@rocket.chat/authorization-service@workspace:ee/apps/authorization-service" + dependencies: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": next + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/string-helpers": next + "@types/eslint": ^8 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + eslint: ^8.21.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + mongodb: ^4.3.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/cas-validate@workspace:^, @rocket.chat/cas-validate@workspace:packages/cas-validate": version: 0.0.0-use.local resolution: "@rocket.chat/cas-validate@workspace:packages/cas-validate" @@ -5605,11 +5666,11 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": next - "@rocket.chat/ui-contexts": "workspace:^" "@types/ejson": ^2.2.0 "@types/eslint": ^8 "@types/meteor": 2.7.1 "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 "@types/sharp": ^0.30.4 "@types/uuid": ^8.3.4 "@types/ws": ^8.5.3 @@ -5617,13 +5678,14 @@ __metadata: ejson: ^2.2.2 eslint: ^8.22.0 eventemitter3: ^4.0.7 - fibers: ^5.0.1 + fibers: ^5.0.3 jaeger-client: ^3.19.0 moleculer: ^0.14.21 mongodb: ^4.3.1 nats: ^2.4.0 pino: ^7.11.0 pino-pretty: ^7.6.1 + polka: ^0.5.2 sharp: ^0.30.7 ts-node: ^10.9.1 typescript: ~4.5.5 @@ -6454,8 +6516,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/presence-service@workspace:ee/apps/presence-service" dependencies: + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/string-helpers": next "@types/eslint": ^8 @@ -6464,7 +6529,7 @@ __metadata: ejson: ^2.2.2 eslint: ^8.21.0 eventemitter3: ^4.0.7 - fibers: ^5.0.1 + fibers: ^5.0.3 moleculer: ^0.14.21 mongodb: ^4.3.1 nats: ^2.4.0 @@ -6487,7 +6552,6 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/ui-contexts": "workspace:^" "@types/node": ^14.18.21 babel-jest: ^29.0.3 eslint: ^8.21.0 @@ -6538,6 +6602,35 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/stream-hub-service@workspace:ee/apps/stream-hub-service": + version: 0.0.0-use.local + resolution: "@rocket.chat/stream-hub-service@workspace:ee/apps/stream-hub-service" + dependencies: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": next + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/string-helpers": next + "@types/bcrypt": ^5.0.0 + "@types/eslint": ^8 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + eslint: ^8.21.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + mongodb: ^4.3.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/string-helpers@npm:next": version: 0.31.19-dev.19 resolution: "@rocket.chat/string-helpers@npm:0.31.19-dev.19" @@ -18513,6 +18606,15 @@ __metadata: languageName: node linkType: hard +"fibers@npm:^5.0.3": + version: 5.0.3 + resolution: "fibers@npm:5.0.3" + dependencies: + detect-libc: ^1.0.3 + checksum: d66c5e18a911aab3480b846e1c837e5c7cfacb27a2a5fe512919865eaecef33cdd4abc14d777191a6a93473dc52356d48549c91a2a7b8b3450544c44104b23f3 + languageName: node + linkType: hard + "figgy-pudding@npm:^3.5.1": version: 3.5.2 resolution: "figgy-pudding@npm:3.5.2" @@ -35808,6 +35910,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.0": + version: 9.0.0 + resolution: "uuid@npm:9.0.0" + bin: + uuid: dist/bin/uuid + checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" -- GitLab From 0fc18286b39a9442ef433b4abf4ca422409c4a8d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 23 Sep 2022 00:21:04 -0300 Subject: [PATCH 064/107] Chore: Refactor omnichannel livechat tests (#26929) --- .../tests/e2e/omnichannel-livechat.spec.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts index 1f3b2955ae3..537903a481e 100644 --- a/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-livechat.spec.ts @@ -22,7 +22,8 @@ test.describe('Livechat', () => { let page: Page; test.beforeAll(async ({ browser, api }) => { - await api.post('/livechat/users/agent', { username: 'user1' }); + const statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); + expect(statusCode).toBe(200); page = await browser.newPage(); poLiveChat = new OmnichannelLiveChat(page); @@ -36,8 +37,8 @@ test.describe('Livechat', () => { await poAuxContext.page.close(); }); - test.describe('Send message to online agent', () => { - test('Expect message to be sent by livechat', async () => { + test('Send message to online agent', async () => { + await test.step('Expect message to be sent by livechat', async () => { await poLiveChat.btnOpenLiveChat('R').click(); await poLiveChat.sendMessage(newUser, false); @@ -47,30 +48,30 @@ test.describe('Livechat', () => { await expect(page.locator('div >>text="this_a_test_message_from_user"')).toBeVisible(); }); - test('expect message to be received by agent', async () => { + await test.step('expect message to be received by agent', async () => { await poAuxContext.poHomeOmnichannel.sidenav.openChat(newUser.name); await expect(poAuxContext.poHomeOmnichannel.content.lastUserMessage).toBeVisible(); await expect(poAuxContext.poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); }); }); - test.describe('Send message to livechat costumer', () => { - test('Expect message to be sent by agent', async () => { + test('Send message to livechat costumer', async () => { + await test.step('Expect message to be sent by agent', async () => { await poAuxContext.poHomeOmnichannel.content.sendMessage('this_a_test_message_from_agent'); await expect(page.locator('div >>text="this_a_test_message_from_agent"')).toBeVisible(); }); - test('Expect when user minimizes the livechat screen, the composer should be hidden', async () => { + await test.step('Expect when user minimizes the livechat screen, the composer should be hidden', async () => { await poLiveChat.btnOpenLiveChat('R').click(); await expect(page.locator('[contenteditable="true"]')).not.toBeVisible(); }); - test('expect message to be received by minimized livechat', async () => { + await test.step('expect message to be received by minimized livechat', async () => { await poAuxContext.poHomeOmnichannel.content.sendMessage('this_a_test_message_again_from_agent'); await expect(poLiveChat.unreadMessagesBadge(1)).toBeVisible(); }); - test('expect unread messages to be visible after a reload', async () => { + await test.step('expect unread messages to be visible after a reload', async () => { await page.reload(); await expect(poLiveChat.unreadMessagesBadge(1)).toBeVisible(); }); -- GitLab From e1a4fb45601e4e2b174d194d3b7e59703bd6a0b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 01:20:01 -0300 Subject: [PATCH 065/107] Bump actions/cache from 2 to 3.0.1 (#25003) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_and_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ac1c861bebf..07755329fd6 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -92,7 +92,7 @@ jobs: df -h - name: Cache meteor local - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ./apps/meteor/.meteor/local key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} @@ -100,7 +100,7 @@ jobs: meteor-local-cache-${{ runner.os }}- - name: Cache meteor - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.meteor key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} -- GitLab From cfd738a35e95aab3d2f1048bb7af82c66e263dd6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Fri, 23 Sep 2022 09:03:54 -0600 Subject: [PATCH 066/107] Regression: wrong permission on livechat/tags endpoints (#26928) --- .../livechat-enterprise/server/api/tags.ts | 4 +- apps/meteor/tests/data/livechat/priorities.ts | 26 +++ apps/meteor/tests/data/livechat/tags.ts | 26 +++ apps/meteor/tests/data/livechat/triggers.ts | 40 ++++ .../api/livechat/06-integrations.ts | 14 ++ .../end-to-end/api/livechat/08-triggers.ts | 63 +++++-- .../end-to-end/api/livechat/12-priorites.ts | 172 ++++++++++++++++++ .../tests/end-to-end/api/livechat/13-tags.ts | 68 +++++++ 8 files changed, 396 insertions(+), 17 deletions(-) create mode 100644 apps/meteor/tests/data/livechat/priorities.ts create mode 100644 apps/meteor/tests/data/livechat/tags.ts create mode 100644 apps/meteor/tests/data/livechat/triggers.ts create mode 100644 apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts create mode 100644 apps/meteor/tests/end-to-end/api/livechat/13-tags.ts diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts index c64c8a70366..757e4e8f949 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts @@ -3,7 +3,7 @@ import { findTags, findTagById } from './lib/tags'; API.v1.addRoute( 'livechat/tags', - { authRequired: true, permissionsRequired: ['view-l-room', 'manage-livechat-tags'] }, + { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, { async get() { const { offset, count } = this.getPaginationItems(); @@ -27,7 +27,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/tags/:tagId', - { authRequired: true, permissionsRequired: ['view-l-room', 'manage-livechat-tags'] }, + { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, { async get() { const { tagId } = this.urlParams; diff --git a/apps/meteor/tests/data/livechat/priorities.ts b/apps/meteor/tests/data/livechat/priorities.ts new file mode 100644 index 00000000000..06a40e2328f --- /dev/null +++ b/apps/meteor/tests/data/livechat/priorities.ts @@ -0,0 +1,26 @@ +import faker from '@faker-js/faker'; +import { ILivechatPriority } from '@rocket.chat/core-typings'; +import { credentials, methodCall, request } from '../api-data'; +import { DummyResponse } from './utils'; + +export const savePriority = (): Promise<ILivechatPriority> => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:savePriority`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:savePriority', + params: [undefined, { name: faker.name.firstName(), description: faker.lorem.sentence(), dueTimeInMinutes: `${faker.datatype.number({ min: 10 })}` }], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse<string, 'wrapped'>) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; diff --git a/apps/meteor/tests/data/livechat/tags.ts b/apps/meteor/tests/data/livechat/tags.ts new file mode 100644 index 00000000000..5244e0a1f57 --- /dev/null +++ b/apps/meteor/tests/data/livechat/tags.ts @@ -0,0 +1,26 @@ +import faker from '@faker-js/faker'; +import { ILivechatTag } from '@rocket.chat/core-typings'; +import { credentials, methodCall, request } from '../api-data'; +import { DummyResponse } from './utils'; + +export const saveTags = (): Promise<ILivechatTag> => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:saveTag`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveTag', + params: [undefined, { name: faker.name.firstName(), description: faker.lorem.sentence() }, []], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse<string, 'wrapped'>) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; diff --git a/apps/meteor/tests/data/livechat/triggers.ts b/apps/meteor/tests/data/livechat/triggers.ts new file mode 100644 index 00000000000..5c821b4495a --- /dev/null +++ b/apps/meteor/tests/data/livechat/triggers.ts @@ -0,0 +1,40 @@ +import faker from '@faker-js/faker'; +import { ILivechatTrigger } from '@rocket.chat/core-typings'; +import { api, credentials, methodCall, request } from '../api-data'; +import { DummyResponse } from './utils'; + +export const createTrigger = (name: string): Promise<boolean> => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:saveTrigger`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveTrigger', + params: [{ name, description: faker.lorem.sentence(), enabled: true, runOnce: faker.datatype.boolean(), actions: [{ name: 'send-message', params: { msg: faker.lorem.sentence(), name: faker.name.firstName(), sender: faker.helpers.arrayElement(['queue', 'custom']) } }], conditions: [{ name: faker.lorem.word(), value: faker.datatype.number() }] }], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, _res: DummyResponse<boolean, 'unwrapped'>) => { + if (err) { + return reject(err); + } + resolve(true); + }); + }); +}; + +export const fetchTriggers = (): Promise<ILivechatTrigger[]> => { + return new Promise((resolve, reject) => { + request + .get(api('livechat/triggers')) + .set(credentials) + .end((err: Error, res: DummyResponse<ILivechatTrigger[], 'wrapped'>) => { + if (err) { + return reject(err); + } + resolve(res.body.triggers); + }); + }); +}; \ No newline at end of file diff --git a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts index 148901e42e2..af6c3add4b4 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import type { ISetting } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import type { Response } from 'supertest'; @@ -36,6 +37,19 @@ describe('LIVECHAT - Integrations', function () { .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body.settings).to.be.an('array'); + const settingIds = res.body.settings.map((setting: ISetting) => setting._id); + expect(settingIds).to.include.members([ + 'Livechat_webhookUrl', + 'Livechat_secret_token', + 'Livechat_webhook_on_start', + 'Livechat_webhook_on_close', + 'Livechat_webhook_on_chat_taken', + 'Livechat_webhook_on_chat_queued', + 'Livechat_webhook_on_forward', + 'Livechat_webhook_on_offline_msg', + 'Livechat_webhook_on_visitor_message', + 'Livechat_webhook_on_agent_message', + ]); }) .end(done); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts index 8138b03bfb0..948b639ba2b 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts @@ -4,6 +4,7 @@ import { expect } from 'chai'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { createTrigger, fetchTriggers } from '../../../data/livechat/triggers'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; describe('LIVECHAT - triggers', function () { @@ -22,21 +23,30 @@ describe('LIVECHAT - triggers', function () { }); }); it('should return an array of triggers', (done) => { - updatePermission('view-livechat-manager', ['admin']).then(() => { - request - .get(api('livechat/triggers')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('success', true); - expect(res.body.triggers).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); + updatePermission('view-livechat-manager', ['admin']) + .then(() => createTrigger(`test${Date.now()}`)) + .then(() => { + request + .get(api('livechat/triggers')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.triggers).to.be.an('array'); + expect(res.body).to.have.property('count').to.be.greaterThan(0); + expect(res.body.triggers[0]).to.have.property('_id'); + expect(res.body.triggers[0]).to.have.property('name'); + expect(res.body.triggers[0]).to.have.property('description'); + expect(res.body.triggers[0]).to.have.property('enabled', true); + expect(res.body.triggers[0]).to.have.property('runOnce').that.is.a('boolean'); + expect(res.body.triggers[0]).to.have.property('conditions').that.is.an('array').with.lengthOf.greaterThan(0); + expect(res.body.triggers[0]).to.have.property('actions').that.is.an('array').with.lengthOf.greaterThan(0); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + }) + .end(done); + }); }); }); @@ -51,5 +61,28 @@ describe('LIVECHAT - triggers', function () { .end(() => updatePermission('view-livechat-manager', ['admin']).then(done)); }); }); + it('should return null when trigger does not exist', async () => { + await updatePermission('view-livechat-manager', ['admin']); + const response = await request + .get(api('livechat/triggers/invalid-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body.trigger).to.be.null; + }); + it('should return the trigger', async () => { + const triggerName = `test${Date.now()}`; + await createTrigger(triggerName); + const trigger = (await fetchTriggers()).find((t) => t.name === triggerName); + const response = await request + .get(api(`livechat/triggers/${trigger?._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body.trigger).to.be.an('object'); + expect(response.body.trigger).to.have.property('_id', trigger?._id); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts b/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts new file mode 100644 index 00000000000..104db8a8c61 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts @@ -0,0 +1,172 @@ +/* eslint-env mocha */ + +import type { ILivechatPriority } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { savePriority } from '../../../data/livechat/priorities'; +import { createVisitor, createLivechatRoom, takeInquiry } from '../../../data/livechat/rooms'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { IS_EE } from '../../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Priorities', function () { + this.retries(0); + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true) + .then(() => updateSetting('Livechat_Routing_Method', 'Manual_Selection')) + .then(done); + }); + + describe('livechat/priorities', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-priorities', []); + await updatePermission('view-l-room', []); + const response = await request + .get(api('livechat/priorities')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + expect(response.body).to.have.property('success', false); + }); + it('should return an array of priorities', async () => { + await updatePermission('manage-livechat-priorities', ['admin']); + await updatePermission('view-l-room', ['livechat-agent']); + const priority = await savePriority(); + const response = await request + .get(api('livechat/priorities')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body.priorities).to.be.an('array').with.lengthOf.greaterThan(0); + expect(response.body.priorities.find((p: ILivechatPriority) => p._id === priority._id)).to.be.an('object'); + }); + }); + + describe('livechat/priorities/:priorityId', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-priorities', []); + await updatePermission('view-l-room', []); + const response = await request + .get(api('livechat/priorities/123')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + expect(response.body).to.have.property('success', false); + }); + it('should return a priority', async () => { + await updatePermission('manage-livechat-priorities', ['admin']); + await updatePermission('view-l-room', ['livechat-agent']); + const priority = await savePriority(); + const response = await request + .get(api(`livechat/priorities/${priority._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body).to.be.an('object'); + expect(response.body._id).to.be.equal(priority._id); + }); + }); + + describe('livechat/inquiry.prioritize', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-priorities', []); + await updatePermission('view-l-room', []); + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: '123', + priority: '123', + }) + .expect('Content-Type', 'application/json') + .expect(403); + expect(response.body).to.have.property('success', false); + }); + it('should fail if roomId is not in request body', async () => { + await updatePermission('manage-livechat-priorities', ['admin']); + await updatePermission('view-l-room', ['livechat-agent']); + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + priority: '123', + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + }); + it('should fail if roomId is invalid', async () => { + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: '123', + priority: '123', + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + }); + it('should fail if priority is not in request body', async () => { + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: '123', + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + }); + it('should fail if priority is not valid', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: room._id, + priority: '123', + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + }); + it('should fail if inquiry is not queued', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + await takeInquiry(room._id); + + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: room._id, + priority: '123', + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + }); + it('should prioritize an inquiry', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const priority = await savePriority(); + const response = await request + .put(api('livechat/inquiry.prioritize')) + .set(credentials) + .send({ + roomId: room._id, + priority: priority._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + }); + }); +}); diff --git a/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts b/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts new file mode 100644 index 00000000000..4a36bd35114 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts @@ -0,0 +1,68 @@ +/* eslint-env mocha */ + +import { expect } from 'chai'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { saveTags } from '../../../data/livechat/tags'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { IS_EE } from '../../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('[EE] Livechat - Tags', function () { + this.retries(0); + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true).then(done); + }); + + describe('livechat/tags', () => { + it('should throw unauthorized error when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-tags', []); + await updatePermission('view-l-room', []); + const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(403); + expect(response.body).to.have.property('success', false); + }); + it('should return an array of tags', async () => { + await updatePermission('manage-livechat-tags', ['admin']); + await updatePermission('view-l-room', ['livechat-agent']); + const tag = await saveTags(); + const response = await request + .get(api('livechat/tags')) + .set(credentials) + .query({ text: tag.name }) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body.tags).to.be.an('array').with.lengthOf.greaterThan(0); + expect(response.body.tags[0]).to.have.property('_id', tag._id); + }); + }); + + describe('livechat/tags/:tagId', () => { + it('should throw unauthorized error when the user does not have the necessary permission', async () => { + await updatePermission('manage-livechat-tags', []); + await updatePermission('view-l-room', []); + const response = await request.get(api('livechat/tags/123')).set(credentials).expect('Content-Type', 'application/json').expect(403); + expect(response.body).to.have.property('success', false); + }); + it('should return null when the tag does not exist', async () => { + await updatePermission('manage-livechat-tags', ['admin']); + await updatePermission('view-l-room', ['livechat-agent']); + const response = await request.get(api('livechat/tags/123')).set(credentials).expect('Content-Type', 'application/json').expect(200); + expect(response.body.body).to.be.null; + }); + it('should return a tag', async () => { + const tag = await saveTags(); + const response = await request + .get(api(`livechat/tags/${tag._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('_id', tag._id); + expect(response.body).to.have.property('name', tag.name); + expect(response.body).to.have.property('numDepartments', 0); + }); + }); +}); -- GitLab From d8e883b7643f59f1782954245ca8ad4e2f04a662 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Fri, 23 Sep 2022 13:37:55 -0300 Subject: [PATCH 067/107] Chore: Introduce `useQuery` as data source for the `Room` component (#26855) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- apps/meteor/app/ui-sidenav/client/roomList.js | 242 +----------------- .../app/ui-utils/client/lib/MessageAction.ts | 2 +- .../app/lib/CommonRoomTemplateInstance.ts | 2 +- .../views/app/lib/getCommonRoomEvents.ts | 4 +- .../definitions/SubscriptionWithRoom.ts | 36 +++ .../lists/useStreamUpdatesForMessageList.ts | 8 +- apps/meteor/client/hooks/useEndpointData.ts | 2 +- apps/meteor/client/hooks/useReactiveQuery.ts | 70 +++++ .../lib/createReactiveSubscriptionFactory.ts | 27 +- apps/meteor/client/lib/queryClient.ts | 11 +- .../client/lib/rooms/roomCoordinator.ts | 8 - .../client/lib/rooms/roomTypes/livechat.ts | 20 +- .../meteor/client/lib/utils/queueMicrotask.ts | 6 + .../client/providers/ServerProvider.tsx | 6 +- apps/meteor/client/providers/UserProvider.tsx | 5 +- apps/meteor/client/startup/index.ts | 1 + .../startup/notifications/usersNameChanged.ts | 16 +- apps/meteor/client/startup/queryCache.ts | 188 ++++++++++++++ apps/meteor/client/startup/userRoles.ts | 10 +- .../stories/contexts/ServerContextMock.tsx | 6 +- .../chats/contextualBar/ChatsContextualBar.js | 2 +- .../contextualBar/ContactsContextualBar.tsx | 2 +- .../Omnichannel/OmnichannelRoomHeader.tsx | 10 +- .../Header/Omnichannel/VoipRoomHeader.tsx | 10 +- .../views/room/Header/ToolBox/ToolBox.tsx | 3 +- .../client/views/room/MemberListRouter.js | 2 +- .../views/room/MessageList/MessageList.tsx | 123 +++++---- .../MessageList/MessageListErrorBoundary.tsx | 5 + .../components/Toolbox/Toolbox.tsx | 8 +- apps/meteor/client/views/room/Room/Room.tsx | 8 +- .../room/components/VerticalBarOldActions.tsx | 3 +- .../views/room/components/body/RoomBody.tsx | 142 +++++----- .../room/components/body/useMessageContext.ts | 10 +- .../room/components/body/useRoomRoles.ts | 58 ----- .../components/body/useRoomRolesManagement.ts | 108 ++++++++ .../views/room/contexts/RoomAPIContext.ts | 5 + .../client/views/room/contexts/RoomContext.ts | 40 ++- .../views/room/contexts/ToolboxContext.ts | 35 +++ .../room/contextualBar/Apps/AppsWithData.tsx | 2 +- .../AutoTranslate/AutoTranslateWithData.tsx | 2 +- .../contextualBar/Discussions/withData.js | 2 +- .../ExportMessages/ExportMessages.tsx | 2 +- .../Info/EditRoomInfo/EditChannelWithData.js | 2 +- .../Info/RoomInfo/RoomInfoWithData.js | 2 +- .../KeyboardShortcutsWithData.tsx | 2 +- .../NotificationPreferencesWithData.tsx | 2 +- .../room/contextualBar/OTR/OTRWithData.tsx | 2 +- .../PruneMessages/PruneMessagesWithData.tsx | 2 +- .../RoomFiles/RoomFilesWithData.js | 2 +- .../RoomMembers/AddUsers/AddUsersWithData.tsx | 2 +- .../InviteUsers/InviteUsersWithData.tsx | 2 +- .../RoomMembers/RoomMembersWithData.tsx | 2 +- .../room/contextualBar/Threads/ThreadList.tsx | 2 +- .../room/contextualBar/Threads/withData.tsx | 2 +- .../VideoConfList/VideoConfListWithData.tsx | 2 +- .../views/room/lib/Toolbox/ToolboxContext.tsx | 58 ----- .../client/views/room/lib/Toolbox/index.tsx | 2 +- .../ReactionListModal/ReactionListModal.tsx | 7 +- .../views/room/providers/MessageProvider.tsx | 2 +- .../views/room/providers/RoomProvider.tsx | 99 +++---- .../views/room/providers/ToolboxProvider.tsx | 54 +--- .../views/room/threads/ThreadComponent.tsx | 2 +- .../contextualBar/channels/TeamsChannels.js | 2 +- .../contextualBar/info/TeamsInfoWithData.js | 2 +- apps/meteor/definition/IRoomTypeConfig.ts | 1 - .../CannedResponse/CannedResponseList.tsx | 2 +- packages/core-typings/src/ISubscription.ts | 6 +- .../hooks/useVideoConfDataStream.ts | 8 +- .../src/ServerContext/ServerContext.ts | 2 +- .../ui-contexts/src/ServerContext/methods.ts | 1 + packages/ui-contexts/src/hooks/useStream.ts | 2 +- 71 files changed, 799 insertions(+), 729 deletions(-) create mode 100644 apps/meteor/client/definitions/SubscriptionWithRoom.ts create mode 100644 apps/meteor/client/hooks/useReactiveQuery.ts create mode 100644 apps/meteor/client/lib/utils/queueMicrotask.ts create mode 100644 apps/meteor/client/startup/queryCache.ts delete mode 100644 apps/meteor/client/views/room/components/body/useRoomRoles.ts create mode 100644 apps/meteor/client/views/room/components/body/useRoomRolesManagement.ts create mode 100644 apps/meteor/client/views/room/contexts/RoomAPIContext.ts create mode 100644 apps/meteor/client/views/room/contexts/ToolboxContext.ts delete mode 100644 apps/meteor/client/views/room/lib/Toolbox/ToolboxContext.tsx diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js index 422f4a2fdda..08d7dbdd3c0 100644 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ b/apps/meteor/app/ui-sidenav/client/roomList.js @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; -import { callbacks } from '../../../lib/callbacks'; import { UiTextContext } from '../../../definition/IRoomTypeConfig'; -import { ChatSubscription, Rooms, Users, Subscriptions } from '../../models/client'; +import { ChatSubscription, Rooms, Users } from '../../models/client'; import { getUserPreference } from '../../utils'; import { settings } from '../../settings'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; @@ -117,242 +116,3 @@ Template.roomList.helpers({ return roomCoordinator.getRoomDirectives(instance.data.identifier)?.getUiText(UiTextContext.NO_ROOMS_SUBSCRIBED) || 'No_channels_yet'; }, }); - -const getLowerCaseNames = (room, nameDefault = '', fnameDefault = '') => { - const name = room.name || nameDefault; - const fname = room.fname || fnameDefault || name; - return { - lowerCaseName: String(!room.prid ? name : fname).toLowerCase(), - lowerCaseFName: String(fname).toLowerCase(), - }; -}; - -const mergeSubRoom = (subscription) => { - const options = { - fields: { - lm: 1, - lastMessage: 1, - uids: 1, - streamingOptions: 1, - usernames: 1, - usersCount: 1, - topic: 1, - encrypted: 1, - // autoTranslate: 1, - // autoTranslateLanguage: 1, - description: 1, - announcement: 1, - broadcast: 1, - archived: 1, - avatarETag: 1, - retention: 1, - teamId: 1, - teamMain: 1, - msgs: 1, - onHold: 1, - metrics: 1, - muted: 1, - servedBy: 1, - ts: 1, - waitingResponse: 1, - v: 1, - transcriptRequest: 1, - tags: 1, - closedAt: 1, - responseBy: 1, - priorityId: 1, - livechatData: 1, - departmentId: 1, - source: 1, - queuedAt: 1, - federated: 1, - }, - }; - - const room = Rooms.findOne({ _id: subscription.rid }, options) || {}; - - const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt; - - const { - encrypted, - description, - cl, - topic, - announcement, - broadcast, - archived, - avatarETag, - retention, - lastMessage, - streamingOptions, - teamId, - teamMain, - uids, - usernames, - usersCount, - v, - transcriptRequest, - servedBy, - onHold, - tags, - closedAt, - metrics, - muted, - waitingResponse, - responseBy, - priorityId, - livechatData, - departmentId, - ts, - source, - queuedAt, - federated, - } = room; - - subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; - - return Object.assign(subscription, getLowerCaseNames(subscription), { - encrypted, - description, - cl, - topic, - announcement, - broadcast, - archived, - avatarETag, - retention, - lastMessage, - streamingOptions, - teamId, - teamMain, - uids, - usernames, - usersCount, - v, - transcriptRequest, - servedBy, - onHold, - tags, - closedAt, - metrics, - muted, - waitingResponse, - responseBy, - priorityId, - livechatData, - departmentId, - ts, - source, - queuedAt, - federated, - }); -}; - -const mergeRoomSub = (room) => { - const sub = Subscriptions.findOne({ rid: room._id }); - if (!sub) { - return room; - } - - const { - encrypted, - description, - cl, - topic, - announcement, - broadcast, - archived, - avatarETag, - retention, - lastMessage, - streamingOptions, - teamId, - teamMain, - uids, - usernames, - usersCount, - v, - transcriptRequest, - servedBy, - onHold, - tags, - closedAt, - metrics, - muted, - waitingResponse, - responseBy, - priorityId, - livechatData, - departmentId, - ts, - source, - queuedAt, - federated, - } = room; - - Subscriptions.update( - { - rid: room._id, - }, - { - $set: { - encrypted, - description, - cl, - topic, - announcement, - broadcast, - archived, - avatarETag, - retention, - uids, - usernames, - usersCount, - lastMessage, - streamingOptions, - teamId, - teamMain, - v, - transcriptRequest, - servedBy, - onHold, - tags, - closedAt, - metrics, - muted, - waitingResponse, - responseBy, - priorityId, - livechatData, - departmentId, - ts, - source, - queuedAt, - federated, - ...getLowerCaseNames(room, sub.name, sub.fname), - }, - }, - ); - - Subscriptions.update( - { - rid: room._id, - lm: { $lt: room.lm }, - }, - { - $set: { - lm: room.lm, - }, - }, - ); - - return room; -}; - -callbacks.add('cachedCollection-received-rooms', mergeRoomSub); -callbacks.add('cachedCollection-sync-rooms', mergeRoomSub); -callbacks.add('cachedCollection-loadFromServer-rooms', mergeRoomSub); - -callbacks.add('cachedCollection-received-subscriptions', mergeSubRoom); -callbacks.add('cachedCollection-sync-subscriptions', mergeSubRoom); -callbacks.add('cachedCollection-loadFromServer-subscriptions', mergeSubRoom); diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 8fa25ecc88b..4b68a6fbdb3 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -10,7 +10,7 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { Messages, Rooms, Subscriptions } from '../../../models/client'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import type { ToolboxContextValue } from '../../../../client/views/room/lib/Toolbox/ToolboxContext'; +import type { ToolboxContextValue } from '../../../../client/views/room/contexts/ToolboxContext'; const call = (method: string, ...args: any[]): Promise<any> => new Promise((resolve, reject) => { diff --git a/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts b/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts index a5c6f2c9f68..9a6b55c682a 100644 --- a/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts +++ b/apps/meteor/app/ui/client/views/app/lib/CommonRoomTemplateInstance.ts @@ -1,4 +1,4 @@ -import type { ToolboxContextValue } from '../../../../../../client/views/room/lib/Toolbox/ToolboxContext'; +import type { ToolboxContextValue } from '../../../../../../client/views/room/contexts/ToolboxContext'; export type CommonRoomTemplateInstance = { data: { diff --git a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts index dc0819106b8..7c49c0cf877 100644 --- a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts +++ b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts @@ -185,7 +185,7 @@ function handleOpenUserCardButtonClick(event: JQuery.ClickEvent, template: Commo target: event.currentTarget, open: (e: MouseEvent) => { e.preventDefault(); - tabBar.openUserInfo(username); + tabBar.openRoomInfo(username); }, }); } @@ -327,7 +327,7 @@ function handleMentionLinkClick(event: JQuery.ClickEvent, template: CommonRoomTe target: event.currentTarget, open: (e: MouseEvent) => { e.preventDefault(); - tabBar.openUserInfo(username); + tabBar.openRoomInfo(username); }, }); } diff --git a/apps/meteor/client/definitions/SubscriptionWithRoom.ts b/apps/meteor/client/definitions/SubscriptionWithRoom.ts new file mode 100644 index 00000000000..53a69f63b0d --- /dev/null +++ b/apps/meteor/client/definitions/SubscriptionWithRoom.ts @@ -0,0 +1,36 @@ +import { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy, ISubscription } from '@rocket.chat/core-typings'; + +export type SubscriptionWithRoom = ISubscription & + Pick< + IRoom, + | 'description' + | 'cl' + | 'topic' + | 'announcement' + | 'avatarETag' + | 'lastMessage' + | 'streamingOptions' + | 'uids' + | 'usernames' + | 'usersCount' + | 'muted' + | 'federated' + | 'lm' + > & + Pick< + IOmnichannelRoom, + | 'transcriptRequest' + | 'servedBy' + | 'tags' + | 'onHold' + | 'closedAt' + | 'metrics' + | 'waitingResponse' + | 'responseBy' + | 'priorityId' + | 'livechatData' + | 'departmentId' + | 'queuedAt' + > & { + source?: IOmnichannelRoom['source']; + } & Pick<Partial<IRoomWithRetentionPolicy>, 'retention'>; diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index cbda6ac4ee3..8bdc30fc9d0 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -44,17 +44,17 @@ export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IU return; } - const unsubscribeFromRoomMessages = subscribeToRoomMessages<RoomMessagesRidEvent>(rid, (message) => { + const unsubscribeFromRoomMessages = subscribeToRoomMessages(rid, (message: RoomMessagesRidEvent) => { messageList.handle(message); }); - const unsubscribeFromDeleteMessage = subscribeToNotifyRoom<NotifyRoomRidDeleteMessageEvent>(`${rid}/deleteMessage`, ({ _id: mid }) => { + const unsubscribeFromDeleteMessage = subscribeToNotifyRoom(`${rid}/deleteMessage`, ({ _id: mid }: NotifyRoomRidDeleteMessageEvent) => { messageList.remove(mid); }); - const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom<NotifyRoomRidDeleteMessageBulkEvent>( + const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom( `${rid}/deleteMessageBulk`, - (params) => { + (params: NotifyRoomRidDeleteMessageBulkEvent) => { const matchDeleteCriteria = createDeleteCriteria(params); messageList.prune(matchDeleteCriteria); }, diff --git a/apps/meteor/client/hooks/useEndpointData.ts b/apps/meteor/client/hooks/useEndpointData.ts index 8cea0c0b701..498d58819e2 100644 --- a/apps/meteor/client/hooks/useEndpointData.ts +++ b/apps/meteor/client/hooks/useEndpointData.ts @@ -8,7 +8,7 @@ import { AsyncState, useAsyncState } from './useAsyncState'; const log = (name: string): Console['log'] => process.env.NODE_ENV !== 'production' || getConfig('debug') === 'true' - ? (...args): void => console.log(name, ...args) + ? (...args): void => console.warn(name, ...args) : (): void => undefined; const deprecationWarning = log('useEndpointData is deprecated, use @tanstack/react-query instead'); diff --git a/apps/meteor/client/hooks/useReactiveQuery.ts b/apps/meteor/client/hooks/useReactiveQuery.ts new file mode 100644 index 00000000000..02f6061843c --- /dev/null +++ b/apps/meteor/client/hooks/useReactiveQuery.ts @@ -0,0 +1,70 @@ +import { IRole, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import { useQuery, UseQueryOptions, QueryKey, UseQueryResult, useQueryClient, QueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { Roles, RoomRoles, Rooms, Subscriptions, Users } from '../../app/models/client'; + +// For convenience as we want to minimize references to the old client models +const queryableCollections = { + users: Users as Mongo.Collection<IUser>, + rooms: Rooms as Mongo.Collection<IRoom>, + subscriptions: Subscriptions as Mongo.Collection<ISubscription>, + roles: Roles as Mongo.Collection<IRole>, + roomRoles: RoomRoles as Mongo.Collection<Pick<ISubscription, 'rid' | 'u' | 'roles'>>, +} as const; + +const dep = new Tracker.Dependency(); +const reactiveSources = new Set<{ + reactiveQueryFn: (collections: typeof queryableCollections) => unknown | undefined; + queryClient: QueryClient; + queryKey: QueryKey; +}>(); + +export const runReactiveFunctions = (): void => { + if (!Tracker.currentComputation) { + throw new Error('runReactiveFunctions must be called inside a Tracker.autorun'); + } + + dep.depend(); + + for (const { reactiveQueryFn, queryClient, queryKey } of reactiveSources) { + // This tracker will be invalidated when the query data changes + Tracker.autorun((c) => { + const data = reactiveQueryFn(queryableCollections); + if (!c.firstRun) queryClient.setQueryData(queryKey, data); + }); + } +}; + +// While React Query handles all async stuff, we need to handle the reactive stuff ourselves using effects +export const useReactiveQuery = <TQueryFnData, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>( + queryKey: TQueryKey, + reactiveQueryFn: (collections: typeof queryableCollections) => TQueryFnData | undefined, + options?: UseQueryOptions<TQueryFnData, Error, TData, TQueryKey>, +): UseQueryResult<TData, Error> => { + const queryClient = useQueryClient(); + + useEffect(() => { + const reactiveSource = { reactiveQueryFn, queryClient, queryKey }; + + reactiveSources.add(reactiveSource); + dep.changed(); + + return (): void => { + reactiveSources.delete(reactiveSource); + dep.changed(); + }; + }); + + return useQuery( + queryKey, + (): Promise<TQueryFnData> => { + const result = Tracker.nonreactive(() => reactiveQueryFn(queryableCollections)); + + if (result) return Promise.resolve(result); + + return new Promise(() => undefined); + }, + { staleTime: Infinity, ...options }, + ); +}; diff --git a/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts index b8e92de99d0..0b7f19e0359 100644 --- a/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts +++ b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts @@ -1,5 +1,7 @@ import { Tracker } from 'meteor/tracker'; +import { queueMicrotask } from './utils/queueMicrotask'; + interface ISubscriptionFactory<T> { (...args: any[]): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T]; } @@ -7,22 +9,26 @@ interface ISubscriptionFactory<T> { export const createReactiveSubscriptionFactory = <T>(computeCurrentValueWith: (...args: any[]) => T): ISubscriptionFactory<T> => (...args: any[]): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T] => { - const computeCurrentValue = (): T => computeCurrentValueWith(...args); - const callbacks = new Set<() => void>(); - let currentValue = computeCurrentValue(); + let currentValue = computeCurrentValueWith(...args); - const computation = Tracker.autorun(() => { - currentValue = computeCurrentValue(); + const reactiveFn = (): void => { + currentValue = computeCurrentValueWith(...args); callbacks.forEach((callback) => { - Tracker.afterFlush(callback); + queueMicrotask(callback); }); - }); + }; - const { stop } = computation; + let computation: Tracker.Computation | undefined; - computation.stop = (): void => undefined; + queueMicrotask(() => { + if (Tracker.currentComputation) { + throw new Error('Cannot call createReactiveSubscriptionFactory inside a Tracker computation'); + } + + computation = Tracker.autorun(reactiveFn); + }); return [ (callback): (() => void) => { @@ -32,8 +38,7 @@ export const createReactiveSubscriptionFactory = callbacks.delete(callback); if (callbacks.size === 0) { - computation.stop = stop; - computation.stop(); + queueMicrotask(() => computation?.stop()); } }; }, diff --git a/apps/meteor/client/lib/queryClient.ts b/apps/meteor/client/lib/queryClient.ts index 6d46de5917b..e80124a84c1 100644 --- a/apps/meteor/client/lib/queryClient.ts +++ b/apps/meteor/client/lib/queryClient.ts @@ -1,3 +1,12 @@ import { QueryClient } from '@tanstack/react-query'; -export const queryClient = new QueryClient(); +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + onError: console.warn, + }, + mutations: { + onError: console.warn, + }, + }, +}); diff --git a/apps/meteor/client/lib/rooms/roomCoordinator.ts b/apps/meteor/client/lib/rooms/roomCoordinator.ts index f9dd38b47c2..6a44bce3181 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.ts +++ b/apps/meteor/client/lib/rooms/roomCoordinator.ts @@ -9,7 +9,6 @@ import { openRoom } from '../../../app/ui-utils/client/lib/openRoom'; import { RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../../../definition/IRoomTypeConfig'; import type { IRoomTypeConfig, IRoomTypeClientDirectives, RoomIdentification } from '../../../definition/IRoomTypeConfig'; import { RoomCoordinator } from '../../../lib/rooms/coordinator'; -import { ToolboxContextValue } from '../../views/room/lib/Toolbox/ToolboxContext'; import { roomExit } from './roomExit'; class RoomCoordinatorClient extends RoomCoordinator { @@ -27,13 +26,6 @@ class RoomCoordinatorClient extends RoomCoordinator { isGroupChat(_room: Partial<IRoom>): boolean { return false; }, - openCustomProfileTab< - T extends { - tabBar: ToolboxContextValue; - }, - >(_instance: T, _room: IRoom, _username: string): boolean { - return false; - }, getUiText(_context: ValueOf<typeof UiTextContext>): string { return ''; }, diff --git a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts index 8b4c10644d1..d4608c5e1c4 100644 --- a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts +++ b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, AtLeast, ValueOf } from '@rocket.chat/core-typings'; +import type { AtLeast, ValueOf } from '@rocket.chat/core-typings'; import { Session } from 'meteor/session'; import { hasPermission } from '../../../../app/authorization/client'; @@ -31,24 +31,6 @@ roomCoordinator.add(LivechatRoomType, { return room.name || room.fname || (room as any).label; }, - openCustomProfileTab(instance, room, username) { - const omniRoom = room as IOmnichannelRoom; - if (!omniRoom?.v || (omniRoom.v as any).username !== username) { - return false; - } - - /* @TODO Due to route information only updating on `Tracker.afterFlush`, - we found out that calling the tabBar.openUserInfo() method at this point will cause a route change - to the previous route instead of the current one, preventing livechat rooms from being opened. - - As a provisory solution, we're delaying the opening of the contextual bar, - which then ensures that the route info is up to date. Although this solution works, - we need to find a more reliable way of ensuring consistent route changes with up-to-date information. - */ - setTimeout(() => instance.tabBar.openUserInfo(), 0); - return true; - }, - getUiText(context) { switch (context) { case UiTextContext.HIDE_WARNING: diff --git a/apps/meteor/client/lib/utils/queueMicrotask.ts b/apps/meteor/client/lib/utils/queueMicrotask.ts new file mode 100644 index 00000000000..13e4c2bdfb1 --- /dev/null +++ b/apps/meteor/client/lib/utils/queueMicrotask.ts @@ -0,0 +1,6 @@ +// Ponyfill for queueMicrotask +export const queueMicrotask = + (typeof window !== 'undefined' && window.queueMicrotask) || + ((cb: () => void): void => { + Promise.resolve().then(cb); + }); diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index 471545ecf52..6d58b1f082b 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -54,15 +54,15 @@ const getStream = ( retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, -): (<T>(eventName: string, callback: (data: T) => void) => () => void) => { +): (<TEvent extends unknown[]>(eventName: string, callback: (...event: TEvent) => void) => () => void) => { const streamer = Meteor.StreamerCentral.instances[streamName] ? Meteor.StreamerCentral.instances[streamName] : new Meteor.Streamer(streamName, options); return (eventName, callback): (() => void) => { - streamer.on(eventName, callback); + streamer.on(eventName, callback as (...args: any[]) => void); return (): void => { - streamer.removeListener(eventName, callback); + streamer.removeListener(eventName, callback as (...args: any[]) => void); }; }; }; diff --git a/apps/meteor/client/providers/UserProvider.tsx b/apps/meteor/client/providers/UserProvider.tsx index c8a7fdc53ba..9f9812d29a0 100644 --- a/apps/meteor/client/providers/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider.tsx @@ -8,6 +8,7 @@ import { getUserPreference } from '../../app/utils/client'; import { callbacks } from '../../lib/callbacks'; import { useReactiveValue } from '../hooks/useReactiveValue'; import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; +import { call } from '../lib/utils/call'; const getUserId = (): string | null => Meteor.userId(); @@ -26,7 +27,7 @@ const loginWithPassword = (user: string | object, password: string): Promise<voi }); const logout = (): Promise<void> => - new Promise((resolve) => { + new Promise((resolve, reject) => { const user = getUser(); if (!user) { @@ -35,7 +36,7 @@ const logout = (): Promise<void> => Meteor.logout(() => { callbacks.run('afterLogoutCleanUp', user); - Meteor.call('logoutCleanUp', user, resolve); + call('logoutCleanUp', user).then(resolve, reject); }); }); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index d6d005429a6..d980ec9dc47 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -20,6 +20,7 @@ import './notifications'; import './oauth'; import './openedRoom'; import './otr'; +import './queryCache'; import './readMessage'; import './readReceipt'; import './reloadRoomAfterLogin'; diff --git a/apps/meteor/client/startup/notifications/usersNameChanged.ts b/apps/meteor/client/startup/notifications/usersNameChanged.ts index a9cd9b70903..9815422ce96 100644 --- a/apps/meteor/client/startup/notifications/usersNameChanged.ts +++ b/apps/meteor/client/startup/notifications/usersNameChanged.ts @@ -1,7 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import { Messages, RoomRoles, Subscriptions } from '../../../app/models/client'; +import { Messages, Subscriptions } from '../../../app/models/client'; import { Notifications } from '../../../app/notifications/client'; type UsersNameChangedEvent = Partial<IUser>; @@ -51,19 +51,5 @@ Meteor.startup(() => { }, }, ); - - RoomRoles.update( - { - 'u._id': _id, - }, - { - $set: { - 'u.name': name, - }, - }, - { - multi: true, - }, - ); }); }); diff --git a/apps/meteor/client/startup/queryCache.ts b/apps/meteor/client/startup/queryCache.ts new file mode 100644 index 00000000000..b2fe0191be9 --- /dev/null +++ b/apps/meteor/client/startup/queryCache.ts @@ -0,0 +1,188 @@ +import { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy, ISubscription } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import { Tracker } from 'meteor/tracker'; + +import { Rooms, Subscriptions } from '../../app/models/client'; +import { callbacks } from '../../lib/callbacks'; +import { SubscriptionWithRoom } from '../definitions/SubscriptionWithRoom'; +import { runReactiveFunctions } from '../hooks/useReactiveQuery'; + +const getLowerCaseNames = ( + room: Pick<IRoom, 'name' | 'fname' | 'prid'>, + nameDefault = '', + fnameDefault = '', +): { + lowerCaseName: string; + lowerCaseFName: string; +} => { + const name = room.name || nameDefault; + const fname = room.fname || fnameDefault || name; + return { + lowerCaseName: String(!room.prid ? name : fname).toLowerCase(), + lowerCaseFName: String(fname).toLowerCase(), + }; +}; + +const mergeSubRoom = (subscription: ISubscription): SubscriptionWithRoom => { + const options = { + fields: { + lm: 1, + lastMessage: 1, + uids: 1, + streamingOptions: 1, + usernames: 1, + usersCount: 1, + topic: 1, + encrypted: 1, + description: 1, + announcement: 1, + broadcast: 1, + archived: 1, + avatarETag: 1, + retention: 1, + teamId: 1, + teamMain: 1, + msgs: 1, + onHold: 1, + metrics: 1, + muted: 1, + servedBy: 1, + ts: 1, + waitingResponse: 1, + v: 1, + transcriptRequest: 1, + tags: 1, + closedAt: 1, + responseBy: 1, + priorityId: 1, + livechatData: 1, + departmentId: 1, + source: 1, + queuedAt: 1, + federated: 1, + }, + }; + + const room = (Rooms as Mongo.Collection<IRoom>).findOne({ _id: subscription.rid }, options); + + const lastRoomUpdate = room?.lm || subscription.ts || subscription._updatedAt; + + return { + ...subscription, + ...getLowerCaseNames(subscription), + encrypted: room?.encrypted, + description: room?.description, + cl: room?.cl, + topic: room?.topic, + announcement: room?.announcement, + broadcast: room?.broadcast, + archived: room?.archived, + avatarETag: room?.avatarETag, + retention: (room as IRoomWithRetentionPolicy | undefined)?.retention, + lastMessage: room?.lastMessage, + streamingOptions: room?.streamingOptions, + teamId: room?.teamId, + teamMain: room?.teamMain, + uids: room?.uids, + usernames: room?.usernames, + usersCount: room?.usersCount ?? 0, + v: (room as IOmnichannelRoom | undefined)?.v, + transcriptRequest: (room as IOmnichannelRoom | undefined)?.transcriptRequest, + servedBy: (room as IOmnichannelRoom | undefined)?.servedBy, + onHold: (room as IOmnichannelRoom | undefined)?.onHold, + tags: (room as IOmnichannelRoom | undefined)?.tags, + closedAt: (room as IOmnichannelRoom | undefined)?.closedAt, + metrics: (room as IOmnichannelRoom | undefined)?.metrics, + muted: room?.muted, + waitingResponse: (room as IOmnichannelRoom | undefined)?.waitingResponse, + responseBy: (room as IOmnichannelRoom | undefined)?.responseBy, + priorityId: (room as IOmnichannelRoom | undefined)?.priorityId, + livechatData: (room as IOmnichannelRoom | undefined)?.livechatData, + departmentId: (room as IOmnichannelRoom | undefined)?.departmentId, + ts: room?.ts ?? subscription.ts, + source: (room as IOmnichannelRoom | undefined)?.source, + queuedAt: (room as IOmnichannelRoom | undefined)?.queuedAt, + federated: room?.federated, + lm: subscription.lr ? new Date(Math.max(subscription.lr.getTime(), lastRoomUpdate.getTime())) : lastRoomUpdate, + }; +}; + +const mergeRoomSub = (room: IRoom): IRoom => { + const sub = (Subscriptions as Mongo.Collection<ISubscription>).findOne({ rid: room._id }); + if (!sub) { + return room; + } + + (Subscriptions as Mongo.Collection<ISubscription>).update( + { + rid: room._id, + }, + { + $set: { + encrypted: room.encrypted, + description: room.description, + cl: room.cl, + topic: room.topic, + announcement: room.announcement, + broadcast: room.broadcast, + archived: room.archived, + avatarETag: room.avatarETag, + retention: (room as IRoomWithRetentionPolicy | undefined)?.retention, + uids: room.uids, + usernames: room.usernames, + usersCount: room.usersCount, + lastMessage: room.lastMessage, + streamingOptions: room.streamingOptions, + teamId: room.teamId, + teamMain: room.teamMain, + v: (room as IOmnichannelRoom | undefined)?.v, + transcriptRequest: (room as IOmnichannelRoom | undefined)?.transcriptRequest, + servedBy: (room as IOmnichannelRoom | undefined)?.servedBy, + onHold: (room as IOmnichannelRoom | undefined)?.onHold, + tags: (room as IOmnichannelRoom | undefined)?.tags, + closedAt: (room as IOmnichannelRoom | undefined)?.closedAt, + metrics: (room as IOmnichannelRoom | undefined)?.metrics, + muted: room.muted, + waitingResponse: (room as IOmnichannelRoom | undefined)?.waitingResponse, + responseBy: (room as IOmnichannelRoom | undefined)?.responseBy, + priorityId: (room as IOmnichannelRoom | undefined)?.priorityId, + livechatData: (room as IOmnichannelRoom | undefined)?.livechatData, + departmentId: (room as IOmnichannelRoom | undefined)?.departmentId, + ts: room.ts, + source: (room as IOmnichannelRoom | undefined)?.source, + queuedAt: (room as IOmnichannelRoom | undefined)?.queuedAt, + federated: room.federated, + ...getLowerCaseNames(room, sub.name, sub.fname), + }, + }, + ); + + (Subscriptions as Mongo.Collection<ISubscription>).update( + { + rid: room._id, + lm: { $lt: room.lm }, + }, + { + $set: { + lm: room.lm, + }, + }, + ); + + return room; +}; + +callbacks.add('cachedCollection-received-rooms', mergeRoomSub); +callbacks.add('cachedCollection-sync-rooms', mergeRoomSub); +callbacks.add('cachedCollection-loadFromServer-rooms', mergeRoomSub); + +callbacks.add('cachedCollection-received-subscriptions', mergeSubRoom); +callbacks.add('cachedCollection-sync-subscriptions', mergeSubRoom); +callbacks.add('cachedCollection-loadFromServer-subscriptions', mergeSubRoom); + +Meteor.startup(() => { + Tracker.autorun(() => { + runReactiveFunctions(); + }); +}); diff --git a/apps/meteor/client/startup/userRoles.ts b/apps/meteor/client/startup/userRoles.ts index 206e36a6d1e..1dc247dc532 100644 --- a/apps/meteor/client/startup/userRoles.ts +++ b/apps/meteor/client/startup/userRoles.ts @@ -2,7 +2,7 @@ import type { IRocketChatRecord, IRole, IUser } from '@rocket.chat/core-typings' import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { UserRoles, RoomRoles, ChatMessage } from '../../app/models/client'; +import { UserRoles, ChatMessage } from '../../app/models/client'; import { Notifications } from '../../app/notifications/client'; import { dispatchToastMessage } from '../lib/toast'; @@ -24,9 +24,7 @@ Meteor.startup(() => { 'roles-change', (role: { type: 'added' | 'removed' | 'changed'; _id: IRole['_id']; u: Partial<IUser>; scope: IRole['scope'] }) => { if (role.type === 'added') { - if (role.scope) { - RoomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); - } else { + if (!role.scope) { UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); ChatMessage.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); } @@ -35,9 +33,7 @@ Meteor.startup(() => { } if (role.type === 'removed') { - if (role.scope) { - RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); - } else { + if (!role.scope) { UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); ChatMessage.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); } diff --git a/apps/meteor/client/stories/contexts/ServerContextMock.tsx b/apps/meteor/client/stories/contexts/ServerContextMock.tsx index 0794a47b7a6..1e8c1dc7a65 100644 --- a/apps/meteor/client/stories/contexts/ServerContextMock.tsx +++ b/apps/meteor/client/stories/contexts/ServerContextMock.tsx @@ -18,14 +18,14 @@ const getStream = ( retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; } = {}, -): (<T>(eventName: string, callback: (data: T) => void) => () => void) => { +): (<TEvent extends unknown[]>(eventName: string, callback: (...event: TEvent) => void) => () => void) => { logAction('getStream', streamName, options); - return (eventName, callback): (() => void) => { + return (eventName: string, callback: () => void): (() => void) => { const subId = Math.random().toString(16).slice(2); logAction('getStream.subscribe', streamName, eventName, subId); - randomDelay().then(() => callback(undefined as any)); + randomDelay().then(() => callback()); return (): void => { logAction('getStream.unsubscribe', streamName, eventName, subId); diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.js index 8140a8025d0..9f6b3fff364 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.js +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.js @@ -2,7 +2,7 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-con import React from 'react'; import VerticalBar from '../../../../../components/VerticalBar'; -import { useTabBarClose } from '../../../../room/providers/ToolboxProvider'; +import { useTabBarClose } from '../../../../room/contexts/ToolboxContext'; import ChatInfo from './ChatInfo'; import RoomEditWithData from './RoomEditWithData'; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx index a2e0bcb0a6f..98a592913e3 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx @@ -4,7 +4,7 @@ import React, { FC } from 'react'; import VerticalBar from '../../../../../components/VerticalBar'; import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext'; -import { useTabBarClose } from '../../../../room/providers/ToolboxProvider'; +import { useTabBarClose } from '../../../../room/contexts/ToolboxContext'; import ContactEditWithData from './ContactEditWithData'; import ContactInfo from './ContactInfo'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx index bf45e3d3a8f..4a444617ce4 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx @@ -4,8 +4,8 @@ import React, { FC, useMemo } from 'react'; import BurgerMenu from '../../../../components/BurgerMenu'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; +import { ToolboxContext, useToolboxContext } from '../../contexts/ToolboxContext'; import { ToolboxActionConfig } from '../../lib/Toolbox'; -import { ToolboxContext, useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; import RoomHeader from '../RoomHeader'; import { BackButton } from './BackButton'; import QuickActions from './QuickActions'; @@ -31,7 +31,7 @@ const OmnichannelRoomHeader: FC<OmnichannelRoomHeaderProps> = ({ slots: parentSl const { isMobile } = useLayout(); const room = useOmnichannelRoom(); const { visibleActions, getAction } = useQuickActions(room); - const context = useToolboxContext(); + const toolbox = useToolboxContext(); const slots = useMemo( () => ({ @@ -50,7 +50,7 @@ const OmnichannelRoomHeader: FC<OmnichannelRoomHeaderProps> = ({ slots: parentSl <ToolboxContext.Provider value={useMemo( () => ({ - ...context, + ...toolbox, actions: new Map([ ...(isMobile ? (visibleActions.map((action) => [ @@ -62,10 +62,10 @@ const OmnichannelRoomHeader: FC<OmnichannelRoomHeaderProps> = ({ slots: parentSl }, ]) as [string, ToolboxActionConfig][]) : []), - ...(Array.from(context.actions.entries()) as [string, ToolboxActionConfig][]), + ...(Array.from(toolbox.actions.entries()) as [string, ToolboxActionConfig][]), ]), }), - [context, isMobile, visibleActions, getAction], + [toolbox, isMobile, visibleActions, getAction], )} > <RoomHeader slots={slots} room={room} /> diff --git a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx index 716e174ba65..50811462902 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx @@ -5,8 +5,8 @@ import React, { FC, useMemo } from 'react'; import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import BurgerMenu from '../../../../components/BurgerMenu'; +import { ToolboxContext, useToolboxContext } from '../../contexts/ToolboxContext'; import { ToolboxActionConfig } from '../../lib/Toolbox'; -import { ToolboxContext, useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; import RoomHeader, { RoomHeaderProps } from '../RoomHeader'; import { BackButton } from './BackButton'; @@ -17,7 +17,7 @@ export type VoipRoomHeaderProps = { const VoipRoomHeader: FC<VoipRoomHeaderProps> = ({ slots: parentSlot, room }) => { const [name] = useCurrentRoute(); const { isMobile } = useLayout(); - const context = useToolboxContext(); + const toolbox = useToolboxContext(); const slots = useMemo( () => ({ @@ -35,10 +35,10 @@ const VoipRoomHeader: FC<VoipRoomHeaderProps> = ({ slots: parentSlot, room }) => <ToolboxContext.Provider value={useMemo( () => ({ - ...context, - actions: new Map([...(Array.from(context.actions.entries()) as [string, ToolboxActionConfig][])]), + ...toolbox, + actions: new Map([...(Array.from(toolbox.actions.entries()) as [string, ToolboxActionConfig][])]), }), - [context], + [toolbox], )} > <RoomHeader slots={slots} room={{ ...room, name: parseOutboundPhoneNumber(room.fname) }} /> diff --git a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx index 7a57c61f796..9256e55cf19 100644 --- a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx +++ b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx @@ -6,9 +6,8 @@ import { TranslationKey, useLayout, useTranslation } from '@rocket.chat/ui-conte import React, { memo, ReactNode, useRef, ComponentProps, ReactElement } from 'react'; // used to open the menu option by keyboard +import { useToolboxContext, useTab, useTabBarOpen } from '../../contexts/ToolboxContext'; import { ToolboxActionConfig, OptionRenderer } from '../../lib/Toolbox'; -import { useToolboxContext } from '../../lib/Toolbox/ToolboxContext'; -import { useTab, useTabBarOpen } from '../../providers/ToolboxProvider'; const renderMenuOption: OptionRenderer = ({ label: { title, icon }, ...props }: any): ReactNode => ( <Option label={title} icon={icon} data-qa-id={`ToolBoxAction-${icon}`} {...props} /> diff --git a/apps/meteor/client/views/room/MemberListRouter.js b/apps/meteor/client/views/room/MemberListRouter.js index 3cbf7ec9462..78651b0f88d 100644 --- a/apps/meteor/client/views/room/MemberListRouter.js +++ b/apps/meteor/client/views/room/MemberListRouter.js @@ -2,9 +2,9 @@ import { useUserId } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useRoom } from './contexts/RoomContext'; +import { useTab, useTabBarClose, useTabContext } from './contexts/ToolboxContext'; import RoomMembers from './contextualBar/RoomMembers'; import UserInfo from './contextualBar/UserInfo'; -import { useTab, useTabBarClose, useTabContext } from './providers/ToolboxProvider'; const getUid = (room, ownUserId) => { if (room.uids?.length === 1) { diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index fc321bdbc27..c86cee3ccf6 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -7,7 +7,6 @@ import { MessageTypes } from '../../../../app/ui-utils/client'; import { useFormatDate } from '../../../hooks/useFormatDate'; import { MessageProvider } from '../providers/MessageProvider'; import { SelectedMessagesProvider } from '../providers/SelectedMessagesProvider'; -import MessageListErrorBoundary from './MessageListErrorBoundary'; import Message from './components/Message'; import MessageSystem from './components/MessageSystem'; import { ThreadMessagePreview } from './components/ThreadMessagePreview'; @@ -32,77 +31,75 @@ export const MessageList = ({ rid }: MessageListProps): ReactElement => { const format = useFormatDate(); return ( - <MessageListErrorBoundary> - <MessageListProvider rid={rid}> - <MessageProvider rid={rid} broadcast={isBroadcast}> - <SelectedMessagesProvider> - <MessageHighlightProvider> - {messages.map((message, index, arr) => { - const previous = arr[index - 1]; + <MessageListProvider rid={rid}> + <MessageProvider rid={rid} broadcast={isBroadcast}> + <SelectedMessagesProvider> + <MessageHighlightProvider> + {messages.map((message, index, arr) => { + const previous = arr[index - 1]; - const isSequential = isMessageSequential(message, previous, messageGroupingPeriod); + const isSequential = isMessageSequential(message, previous, messageGroupingPeriod); - const isNewDay = isMessageNewDay(message, previous); - const isFirstUnread = isMessageFirstUnread(subscription, message, previous); - const isUserOwnMessage = isOwnUserMessage(message, subscription); - const shouldShowDivider = isNewDay || isFirstUnread; + const isNewDay = isMessageNewDay(message, previous); + const isFirstUnread = isMessageFirstUnread(subscription, message, previous); + const isUserOwnMessage = isOwnUserMessage(message, subscription); + const shouldShowDivider = isNewDay || isFirstUnread; - const shouldShowAsSequential = isSequential && !isNewDay; + const shouldShowAsSequential = isSequential && !isNewDay; - const isSystemMessage = MessageTypes.isSystemMessage(message); - const shouldShowMessage = !isThreadMessage(message) && !isSystemMessage; + const isSystemMessage = MessageTypes.isSystemMessage(message); + const shouldShowMessage = !isThreadMessage(message) && !isSystemMessage; - const unread = Boolean(subscription?.tunread?.includes(message._id)); - const mention = Boolean(subscription?.tunreadUser?.includes(message._id)); - const all = Boolean(subscription?.tunreadGroup?.includes(message._id)); + const unread = Boolean(subscription?.tunread?.includes(message._id)); + const mention = Boolean(subscription?.tunreadUser?.includes(message._id)); + const all = Boolean(subscription?.tunreadGroup?.includes(message._id)); - return ( - <Fragment key={message._id}> - {shouldShowDivider && ( - <MessageDivider unreadLabel={isFirstUnread ? t('Unread_Messages').toLowerCase() : undefined}> - {isNewDay && format(message.ts)} - </MessageDivider> - )} + return ( + <Fragment key={message._id}> + {shouldShowDivider && ( + <MessageDivider unreadLabel={isFirstUnread ? t('Unread_Messages').toLowerCase() : undefined}> + {isNewDay && format(message.ts)} + </MessageDivider> + )} - {shouldShowMessage && ( - <Message - id={message._id} - data-id={message._id} - data-system-message={Boolean(message.t)} - data-mid={message._id} - data-unread={isFirstUnread} - data-sequential={isSequential} - data-own={isUserOwnMessage} - data-qa-type='message' - sequential={shouldShowAsSequential} - message={message} - unread={unread} - mention={mention} - all={all} - /> - )} + {shouldShowMessage && ( + <Message + id={message._id} + data-id={message._id} + data-system-message={Boolean(message.t)} + data-mid={message._id} + data-unread={isFirstUnread} + data-sequential={isSequential} + data-own={isUserOwnMessage} + data-qa-type='message' + sequential={shouldShowAsSequential} + message={message} + unread={unread} + mention={mention} + all={all} + /> + )} - {isThreadMessage(message) && ( - <ThreadMessagePreview - data-system-message={Boolean(message.t)} - data-mid={message._id} - data-tmid={message.tmid} - data-unread={isFirstUnread} - data-sequential={isSequential} - sequential={shouldShowAsSequential} - message={message as IThreadMessage} - /> - )} + {isThreadMessage(message) && ( + <ThreadMessagePreview + data-system-message={Boolean(message.t)} + data-mid={message._id} + data-tmid={message.tmid} + data-unread={isFirstUnread} + data-sequential={isSequential} + sequential={shouldShowAsSequential} + message={message as IThreadMessage} + /> + )} - {isSystemMessage && <MessageSystem message={message} />} - </Fragment> - ); - })} - </MessageHighlightProvider> - </SelectedMessagesProvider> - </MessageProvider> - </MessageListProvider> - </MessageListErrorBoundary> + {isSystemMessage && <MessageSystem message={message} />} + </Fragment> + ); + })} + </MessageHighlightProvider> + </SelectedMessagesProvider> + </MessageProvider> + </MessageListProvider> ); }; diff --git a/apps/meteor/client/views/room/MessageList/MessageListErrorBoundary.tsx b/apps/meteor/client/views/room/MessageList/MessageListErrorBoundary.tsx index a312fe5a221..4803e54777c 100644 --- a/apps/meteor/client/views/room/MessageList/MessageListErrorBoundary.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageListErrorBoundary.tsx @@ -3,11 +3,16 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, ReactNode } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import { useRoom } from '../contexts/RoomContext'; + const MessageListErrorBoundary = ({ children }: { children: ReactNode }): ReactElement => { const t = useTranslation(); + const room = useRoom(); + return ( <ErrorBoundary children={children} + resetKeys={[room._id]} fallback={ <States> <StatesIcon name='circle-exclamation' variation='danger' /> diff --git a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx index fd9e259c67b..3946c45b074 100644 --- a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx +++ b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx @@ -5,7 +5,7 @@ import React, { FC, memo, useMemo } from 'react'; import { MessageAction } from '../../../../../../app/ui-utils/client/lib/MessageAction'; import { useRoom } from '../../../contexts/RoomContext'; -import { getTabBarContext } from '../../../lib/Toolbox/ToolboxContext'; +import { useToolboxContext } from '../../../contexts/ToolboxContext'; import { useIsSelecting } from '../../contexts/SelectedMessagesContext'; import { MessageActionMenu } from './MessageActionMenu'; @@ -26,7 +26,7 @@ export const Toolbox: FC<{ message: IMessage }> = ({ message }) => { const menuActions = MessageAction.getButtons({ message, room, user, subscription, settings: mapSettings }, context, 'menu'); - const tabbar = getTabBarContext(message.rid); + const toolbox = useToolboxContext(); const isSelecting = useIsSelecting(); @@ -39,7 +39,7 @@ export const Toolbox: FC<{ message: IMessage }> = ({ message }) => { {messageActions.map((action) => ( <MessageToolboxItem onClick={(e): void => { - action.action(e, { message, tabbar, room }); + action.action(e, { message, tabbar: toolbox, room }); }} key={action.id} icon={action.icon} @@ -53,7 +53,7 @@ export const Toolbox: FC<{ message: IMessage }> = ({ message }) => { options={menuActions.map((action) => ({ ...action, action: (e): void => { - action.action(e, { message, tabbar, room }); + action.action(e, { message, tabbar: toolbox, room }); }, }))} data-qa-type='message-action-menu-options' diff --git a/apps/meteor/client/views/room/Room/Room.tsx b/apps/meteor/client/views/room/Room/Room.tsx index 55435c30257..363cc5bf9bd 100644 --- a/apps/meteor/client/views/room/Room/Room.tsx +++ b/apps/meteor/client/views/room/Room/Room.tsx @@ -7,18 +7,18 @@ import Header from '../Header'; import VerticalBarOldActions from '../components/VerticalBarOldActions'; import RoomBody from '../components/body/RoomBody'; import { useRoom } from '../contexts/RoomContext'; +import { useTab, useToolboxContext } from '../contexts/ToolboxContext'; import AppsContextualBar from '../contextualBar/Apps'; import { useAppsContextualBar } from '../hooks/useAppsContextualBar'; import RoomLayout from '../layout/RoomLayout'; import { SelectedMessagesProvider } from '../providers/SelectedMessagesProvider'; -import { useTab, useTabBarAPI } from '../providers/ToolboxProvider'; const Room = (): ReactElement => { const t = useTranslation(); const room = useRoom(); - const tabBar = useTabBarAPI(); + const toolbox = useToolboxContext(); const tab = useTab(); const appsContextualBarContext = useAppsContextualBar(); @@ -34,11 +34,11 @@ const Room = (): ReactElement => { <ErrorBoundary fallback={null}> <SelectedMessagesProvider> {typeof tab.template === 'string' && ( - <VerticalBarOldActions {...tab} name={tab.template} tabBar={tabBar} rid={room._id} _id={room._id} /> + <VerticalBarOldActions {...tab} name={tab.template} tabBar={toolbox} rid={room._id} _id={room._id} /> )} {typeof tab.template !== 'string' && typeof tab.template !== 'undefined' && ( <Suspense fallback={<VerticalBarSkeleton />}> - {createElement(tab.template, { tabBar, _id: room._id, rid: room._id, teamId: room.teamId })} + {createElement(tab.template, { tabBar: toolbox, _id: room._id, rid: room._id, teamId: room.teamId })} </Suspense> )} </SelectedMessagesProvider> diff --git a/apps/meteor/client/views/room/components/VerticalBarOldActions.tsx b/apps/meteor/client/views/room/components/VerticalBarOldActions.tsx index 9bdfa68395e..e41b3abb2a0 100644 --- a/apps/meteor/client/views/room/components/VerticalBarOldActions.tsx +++ b/apps/meteor/client/views/room/components/VerticalBarOldActions.tsx @@ -4,8 +4,7 @@ import { useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; import React, { ReactElement, ComponentProps } from 'react'; import VerticalBar from '../../../components/VerticalBar'; -import { ToolboxContextValue } from '../lib/Toolbox/ToolboxContext'; -import { useTabBarClose } from '../providers/ToolboxProvider'; +import { ToolboxContextValue, useTabBarClose } from '../contexts/ToolboxContext'; import BlazeTemplate from './BlazeTemplate'; type VerticalBarOldActionsProps = { diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 3b633719d34..d8c4f7c1d2a 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -1,4 +1,4 @@ -import { IMessage, isEditedMessage, isOmnichannelRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import { IMessage, isEditedMessage, isOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { useCurrentRoute, usePermission, @@ -12,7 +12,7 @@ import { } from '@rocket.chat/ui-contexts'; import React, { memo, ReactElement, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Subscriptions, ChatMessage, RoomRoles, Users } from '../../../../../app/models/client'; +import { ChatMessage } from '../../../../../app/models/client'; import { readMessage, RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { openUserCard } from '../../../../../app/ui/client/lib/UserCard'; import { Uploading } from '../../../../../app/ui/client/lib/fileUpload'; @@ -23,13 +23,14 @@ import { callbacks } from '../../../../../lib/callbacks'; import { isTruthy } from '../../../../../lib/isTruthy'; import { withDebouncing, withThrottling } from '../../../../../lib/utils/highOrderFunctions'; import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { useReactiveQuery } from '../../../../hooks/useReactiveQuery'; import { RoomManager as NewRoomManager } from '../../../../lib/RoomManager'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import Announcement from '../../Announcement'; import { MessageList } from '../../MessageList/MessageList'; -import { useRoom } from '../../contexts/RoomContext'; -import { useTabBarAPI } from '../../providers/ToolboxProvider'; +import MessageListErrorBoundary from '../../MessageList/MessageListErrorBoundary'; +import { useRoom, useRoomSubscription, useRoomMessages } from '../../contexts/RoomContext'; +import { useToolboxContext } from '../../contexts/ToolboxContext'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessagesBar from './JumpToRecentMessagesBar'; import LeaderBar from './LeaderBar'; @@ -44,7 +45,6 @@ import ComposerContainer from './composer/ComposerContainer'; import { useChatMessages } from './useChatMessages'; import { useFileUploadDropTarget } from './useFileUploadDropTarget'; import { useRetentionPolicy } from './useRetentionPolicy'; -import { useRoomRoles } from './useRoomRoles'; import { useUnreadMessages } from './useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -52,11 +52,9 @@ const RoomBody = (): ReactElement => { const isLayoutEmbedded = useEmbeddedLayout(); const room = useRoom(); const user = useUser(); - const tabBar = useTabBarAPI(); + const toolbox = useToolboxContext(); const admin = useRole('admin'); - const subscription = useReactiveValue( - useCallback(() => Subscriptions.findOne({ rid: room._id }) as ISubscription | undefined, [room._id]), - ); + const subscription = useRoomSubscription(); const [lastMessage, setLastMessage] = useState<Date | undefined>(); const [hideLeaderHeader, setHideLeaderHeader] = useState(false); @@ -74,7 +72,6 @@ const RoomBody = (): ReactElement => { const lastScrollTopRef = useRef(0); const chatMessagesInstance = useChatMessages(room._id, wrapperRef); - useRoomRoles(room._id); const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(room); const _isAtBottom = useCallback((scrollThreshold = 0) => { @@ -129,11 +126,7 @@ const RoomBody = (): ReactElement => { return modes[viewMode ?? 0] ?? modes[0]; }, [viewMode]); - const hasMore = useReactiveValue(useCallback(() => RoomHistoryManager.hasMore(room._id), [room._id])); - - const hasMoreNext = useReactiveValue(useCallback(() => RoomHistoryManager.hasMoreNext(room._id), [room._id])); - - const isLoading = useReactiveValue(useCallback(() => RoomHistoryManager.isLoading(room._id), [room._id])); + const { hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages } = useRoomMessages(); const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead') as boolean | undefined; @@ -157,36 +150,28 @@ const RoomBody = (): ReactElement => { return subscribed; }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); - const roomRoles = useReactiveValue<ISubscription | undefined>( - useCallback( - () => - RoomRoles.findOne({ - 'rid': room._id, - 'roles': 'leader', - 'u._id': { $ne: user?._id }, - }), - [room._id, user?._id], - ), - ); - const useRealName = useSetting('UI_Use_Real_Name') as boolean; - const roomLeader = useReactiveValue( - useCallback(() => { - if (!roomRoles) { - return; - } + const { data: roomLeader } = useReactiveQuery(['rooms', room._id, 'leader', { not: user?._id }], ({ roomRoles, users }) => { + const leaderRoomRole = roomRoles.findOne({ + 'rid': room._id, + 'roles': 'leader', + 'u._id': { $ne: user?._id }, + }); - const leaderUser = Users.findOne({ _id: roomRoles.u._id }, { fields: { status: 1, statusText: 1 } }) as IUser | undefined; + if (!leaderRoomRole) { + return; + } - return { - ...roomRoles.u, - name: useRealName ? roomRoles.u.name || roomRoles.u.username : roomRoles.u.username, - status: leaderUser?.status, - statusText: leaderUser?.statusText, - } as const; - }, [roomRoles, useRealName]), - ); + const leaderUser = users.findOne({ _id: leaderRoomRole.u._id }, { fields: { status: 1, statusText: 1 } }); + + return { + ...leaderRoomRole.u, + name: useRealName ? leaderRoomRole.u.name || leaderRoomRole.u.username : leaderRoomRole.u.username, + status: leaderUser?.status, + statusText: leaderUser?.statusText, + }; + }); const handleOpenUserCardButtonClick = useCallback( (event: UIEvent, username: IUser['username']) => { @@ -196,11 +181,11 @@ const RoomBody = (): ReactElement => { target: event.currentTarget, open: (event: MouseEvent) => { event.preventDefault(); - if (username) tabBar.openUserInfo(username); + if (username) toolbox.openRoomInfo(username); }, }); }, - [room._id, tabBar], + [room._id, toolbox], ); const handleUnreadBarJumpToButtonClick = useCallback(() => { @@ -268,20 +253,22 @@ const RoomBody = (): ReactElement => { }, [sendToBottomIfNecessary]); const [routeName] = useCurrentRoute(); - const openedRoom = useSession('openedRoom'); + + const roomRef = useRef(room); + roomRef.current = room; + const tabBarRef = useRef(toolbox); + tabBarRef.current = toolbox; useEffect(() => { + const room = roomRef.current; + const tabBar = tabBarRef.current; Tracker.afterFlush(() => { - if (room._id !== openedRoom) { - return; - } - - if (room && isOmnichannelRoom(room) && !tabBar.activeTabBar) { - roomCoordinator.getRoomDirectives(room.t)?.openCustomProfileTab({ tabBar }, room, room.v.username); + // Find a better way to do this, declaratively + if (room && isOmnichannelRoom(room) && tabBar.activeTabBar?.id !== 'room-info') { + tabBar.openRoomInfo(); } }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [openedRoom, room._id]); + }, [room._id]); const debouncedReadMessageRead = useMemo( () => @@ -291,6 +278,8 @@ const RoomBody = (): ReactElement => { [room._id], ); + const openedRoom = useSession('openedRoom'); + useEffect(() => { if (!routeName || !roomCoordinator.isRouteNameKnown(routeName) || room._id !== openedRoom) { return; @@ -358,7 +347,8 @@ const RoomBody = (): ReactElement => { return { event, selector, - listener: (e: JQuery.TriggeredEvent<HTMLUListElement, undefined>) => handler.call(null, e, { data: { rid: room._id, tabBar } }), + listener: (e: JQuery.TriggeredEvent<HTMLUListElement, undefined>) => + handler.call(null, e, { data: { rid: room._id, tabBar: toolbox } }), }; }); @@ -371,7 +361,7 @@ const RoomBody = (): ReactElement => { $(messageList).off(event, selector, listener); } }; - }, [room._id, sendToBottomIfNecessary, tabBar]); + }, [room._id, sendToBottomIfNecessary, toolbox]); useEffect(() => { const wrapper = wrapperRef.current; @@ -540,7 +530,7 @@ const RoomBody = (): ReactElement => { className={`messages-container flex-tab-main-content ${admin ? 'admin' : ''}`} id={`chat-window-${room._id}`} aria-label={t('Channel')} - onClick={hideFlexTab ? tabBar.close : undefined} + onClick={hideFlexTab ? toolbox.close : undefined} > <div className='messages-container-wrapper'> <div className='messages-container-main' {...fileUploadTriggerProps}> @@ -570,7 +560,7 @@ const RoomBody = (): ReactElement => { className={['messages-box', messageViewMode, roomLeader && 'has-leader'].filter(isTruthy).join(' ')} > <NewMessagesButton visible={hasNewMessages} onClick={handleNewMessageButtonClick} /> - <JumpToRecentMessagesBar visible={hasMoreNext} onClick={handleJumpToRecentButtonClick} /> + <JumpToRecentMessagesBar visible={hasMoreNextMessages} onClick={handleJumpToRecentButtonClick} /> {!canPreview ? ( <div className='content room-not-found error-color'> <div>{t('You_must_join_to_view_messages_in_this_channel')}</div> @@ -590,29 +580,33 @@ const RoomBody = (): ReactElement => { ref={wrapperRef} className={[ 'wrapper', - hasMoreNext && 'has-more-next', + hasMoreNextMessages && 'has-more-next', hideUsernames && 'hide-usernames', !displayAvatars && 'hide-avatar', ] .filter(isTruthy) .join(' ')} > - <ul className='messages-list' aria-live='polite'> - {canPreview ? ( - <> - {hasMore ? ( - <li className='load-more'>{isLoading ? <LoadingMessagesIndicator /> : null}</li> - ) : ( - <li className='start color-info-font-color'> - {retentionPolicy ? <RetentionPolicyWarning {...retentionPolicy} /> : null} - <RoomForeword user={user} room={room} /> - </li> - )} - </> - ) : null} - {useLegacyMessageTemplate ? <LegacyMessageTemplateList room={room} /> : <MessageList rid={room._id} />} - {hasMoreNext ? <li className='load-more'>{isLoading ? <LoadingMessagesIndicator /> : null}</li> : null} - </ul> + <MessageListErrorBoundary> + <ul className='messages-list' aria-live='polite'> + {canPreview ? ( + <> + {hasMorePreviousMessages ? ( + <li className='load-more'>{isLoadingMoreMessages ? <LoadingMessagesIndicator /> : null}</li> + ) : ( + <li className='start color-info-font-color'> + {retentionPolicy ? <RetentionPolicyWarning {...retentionPolicy} /> : null} + <RoomForeword user={user} room={room} /> + </li> + )} + </> + ) : null} + {useLegacyMessageTemplate ? <LegacyMessageTemplateList room={room} /> : <MessageList rid={room._id} />} + {hasMoreNextMessages ? ( + <li className='load-more'>{isLoadingMoreMessages ? <LoadingMessagesIndicator /> : null}</li> + ) : null} + </ul> + </MessageListErrorBoundary> </div> </div> <ComposerContainer diff --git a/apps/meteor/client/views/room/components/body/useMessageContext.ts b/apps/meteor/client/views/room/components/body/useMessageContext.ts index 0e897c8db5e..a6a911163ff 100644 --- a/apps/meteor/client/views/room/components/body/useMessageContext.ts +++ b/apps/meteor/client/views/room/components/body/useMessageContext.ts @@ -1,24 +1,24 @@ import { IRoom } from '@rocket.chat/core-typings'; -import { useLayout, usePermission, useSetting, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useLayout, usePermission, useSetting, useUser, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo } from 'react'; import { AutoTranslate } from '../../../../../app/autotranslate/client'; -import { Subscriptions, Users } from '../../../../../app/models/client'; import { createMessageContext } from '../../../../../app/ui-utils/client/lib/messageContext'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { useRoomSubscription } from '../../contexts/RoomContext'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const useMessageContext = (room: IRoom) => { const uid = useUserId(); - const user = useReactiveValue(useCallback(() => Users.findOne({ _id: uid }), [uid])); + const user = useUser() ?? undefined; const rid = room._id; - const subscription = useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }), [rid])); + const subscription = useRoomSubscription(); const { isEmbedded: embeddedLayout, isMobile: mobile } = useLayout(); const translateLanguage = useReactiveValue(useCallback(() => AutoTranslate.getLanguage(rid), [rid])); const autoImageLoad = useUserPreference('autoImageLoad'); const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate'); const saveMobileBandwidth = useUserPreference('saveMobileBandwidth'); - const collapseMediaByDefault = useUserPreference(user, 'collapseMediaByDefault'); + const collapseMediaByDefault = useUserPreference('collapseMediaByDefault'); const hasPermissionDeleteMessage = usePermission('delete-message', rid); const hasPermissionDeleteOwnMessage = usePermission('delete-own-message'); const displayRoles = useSetting('UI_DisplayRoles'); diff --git a/apps/meteor/client/views/room/components/body/useRoomRoles.ts b/apps/meteor/client/views/room/components/body/useRoomRoles.ts deleted file mode 100644 index 073d5dc1a6c..00000000000 --- a/apps/meteor/client/views/room/components/body/useRoomRoles.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IRoom } from '@rocket.chat/core-typings'; -import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; - -import { RoomRoles, ChatMessage } from '../../../../../app/models/client'; - -export const useRoomRoles = (rid: IRoom['_id']): void => { - const queryClient = useQueryClient(); - const getRoomRoles = useMethod('getRoomRoles'); - const dispatchToastMessage = useToastMessageDispatch(); - - useEffect(() => { - queryClient - .fetchQuery({ - queryKey: ['room', rid, 'roles'], - queryFn: () => getRoomRoles(rid), - staleTime: 15_000, - }) - .then((results) => { - Array.from(results).forEach(({ _id, ...data }) => { - const { - rid, - u: { _id: uid }, - } = data; - RoomRoles.upsert({ rid, 'u._id': uid }, data); - }); - }) - .catch((error) => { - dispatchToastMessage({ type: 'error', message: error }); - }); - - const rolesObserve = RoomRoles.find({ rid }).observe({ - added: (role) => { - if (!role.u?._id) { - return; - } - ChatMessage.update({ rid, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - }, - changed: (role) => { - if (!role.u?._id) { - return; - } - ChatMessage.update({ rid, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); - }, - removed: (role) => { - if (!role.u?._id) { - return; - } - ChatMessage.update({ rid, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - }, - }); - - return (): void => { - rolesObserve.stop(); - }; - }, [dispatchToastMessage, getRoomRoles, queryClient, rid]); -}; diff --git a/apps/meteor/client/views/room/components/body/useRoomRolesManagement.ts b/apps/meteor/client/views/room/components/body/useRoomRolesManagement.ts new file mode 100644 index 00000000000..ad1a939a052 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/useRoomRolesManagement.ts @@ -0,0 +1,108 @@ +import { IRole, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import { useMethod, useStream } from '@rocket.chat/ui-contexts'; +import { Mongo } from 'meteor/mongo'; +import { useEffect } from 'react'; + +import { RoomRoles, ChatMessage } from '../../../../../app/models/client'; + +const roomRoles = RoomRoles as Mongo.Collection<Pick<ISubscription, 'rid' | 'u' | 'roles'>>; + +export const useRoomRolesManagement = (rid: IRoom['_id']): void => { + const getRoomRoles = useMethod('getRoomRoles'); + + useEffect(() => { + getRoomRoles(rid).then((results) => { + Array.from(results).forEach(({ _id, ...data }) => { + const { + rid, + u: { _id: uid }, + } = data; + roomRoles.upsert({ rid, 'u._id': uid }, data); + }); + }); + }, [getRoomRoles, rid]); + + useEffect(() => { + const rolesObserve = RoomRoles.find({ rid }).observe({ + added: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); + }, + changed: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); + }, + removed: (role) => { + if (!role.u?._id) { + return; + } + ChatMessage.update({ rid, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); + }, + }); + + return (): void => { + rolesObserve.stop(); + }; + }, [getRoomRoles, rid]); + + const subscribeToNotifyLoggedIn = useStream('notify-logged'); + + useEffect( + () => + subscribeToNotifyLoggedIn( + 'roles-change', + ({ + type, + ...role + }: { + type: 'added' | 'removed' | 'changed'; + _id: IRole['_id']; + u: { + _id: IUser['_id']; + username: IUser['username']; + name: IUser['name']; + }; + scope?: IRoom['_id']; + }) => { + if (!role.scope) { + return; + } + + switch (type) { + case 'added': + roomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); + break; + + case 'removed': + RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); + break; + } + }, + ), + [subscribeToNotifyLoggedIn], + ); + + useEffect( + () => + subscribeToNotifyLoggedIn('Users:NameChanged', ({ _id: uid, name }: Partial<IUser>) => { + roomRoles.update( + { + 'u._id': uid, + }, + { + $set: { + 'u.name': name, + }, + }, + { + multi: true, + }, + ); + }), + [subscribeToNotifyLoggedIn], + ); +}; diff --git a/apps/meteor/client/views/room/contexts/RoomAPIContext.ts b/apps/meteor/client/views/room/contexts/RoomAPIContext.ts new file mode 100644 index 00000000000..2c2b14cac50 --- /dev/null +++ b/apps/meteor/client/views/room/contexts/RoomAPIContext.ts @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +type RoomAPIContextValue = {}; + +export const RoomAPIContext = createContext<RoomAPIContextValue | undefined>(undefined); diff --git a/apps/meteor/client/views/room/contexts/RoomContext.ts b/apps/meteor/client/views/room/contexts/RoomContext.ts index 2d138165db3..368b20d3279 100644 --- a/apps/meteor/client/views/room/contexts/RoomContext.ts +++ b/apps/meteor/client/views/room/contexts/RoomContext.ts @@ -1,13 +1,13 @@ -import { IRoom, IOmnichannelRoom, isOmnichannelRoom, isVoipRoom, IVoipRoom } from '@rocket.chat/core-typings'; +import { IRoom, IOmnichannelRoom, isOmnichannelRoom, isVoipRoom, IVoipRoom, ISubscription } from '@rocket.chat/core-typings'; import { createContext, useContext } from 'react'; export type RoomContextValue = { rid: IRoom['_id']; - // room: IRoom; - // events: any; - // tabBar: TabBar; room: IRoom; - subscribed: boolean; + subscription?: ISubscription; + hasMorePreviousMessages: boolean; + hasMoreNextMessages: boolean; + isLoadingMoreMessages: boolean; }; export const RoomContext = createContext<RoomContextValue | null>(null); @@ -19,7 +19,7 @@ export const useUserIsSubscribed = (): boolean => { throw new Error('use useRoom only inside opened rooms'); } - return context.subscribed ?? false; + return !!context.subscription; }; export const useRoom = (): IRoom => { @@ -32,6 +32,34 @@ export const useRoom = (): IRoom => { return room; }; +export const useRoomSubscription = (): ISubscription | undefined => { + const context = useContext(RoomContext); + + if (!context) { + throw new Error('use useRoomSubscription only inside opened rooms'); + } + + return context.subscription; +}; + +export const useRoomMessages = (): { + hasMorePreviousMessages: boolean; + hasMoreNextMessages: boolean; + isLoadingMoreMessages: boolean; +} => { + const context = useContext(RoomContext); + + if (!context) { + throw new Error('use useRoomMessages only inside opened rooms'); + } + + return { + hasMorePreviousMessages: context.hasMorePreviousMessages, + hasMoreNextMessages: context.hasMoreNextMessages, + isLoadingMoreMessages: context.isLoadingMoreMessages, + }; +}; + export const useOmnichannelRoom = (): IOmnichannelRoom => { const { room } = useContext(RoomContext) || {}; diff --git a/apps/meteor/client/views/room/contexts/ToolboxContext.ts b/apps/meteor/client/views/room/contexts/ToolboxContext.ts new file mode 100644 index 00000000000..73a9748d9ab --- /dev/null +++ b/apps/meteor/client/views/room/contexts/ToolboxContext.ts @@ -0,0 +1,35 @@ +import { EventHandlerOf } from '@rocket.chat/emitter'; +import { createContext, useContext } from 'react'; + +import { actions, listen, ToolboxActionConfig, ToolboxAction, Events } from '../lib/Toolbox'; +import '../lib/Toolbox/defaultActions'; + +export type ToolboxEventHandler = (handler: EventHandlerOf<Events, 'change'>) => () => void; + +export type ToolboxContextValue = { + actions: Map<ToolboxActionConfig['id'], ToolboxAction>; + listen: ToolboxEventHandler; + tabBar?: any; + context?: any; + open: (actionId: string, context?: string) => void; + openRoomInfo: (username?: string) => void; + close: () => void; + activeTabBar?: ToolboxActionConfig; + setData?: (data: Record<string, unknown>) => void; +}; + +export const ToolboxContext = createContext<ToolboxContextValue>({ + actions, + listen, + open: () => undefined, + openRoomInfo: () => undefined, + close: () => undefined, +}); + +export const useToolboxContext = (): ToolboxContextValue => useContext(ToolboxContext); + +export const useTabContext = (): unknown | undefined => useContext(ToolboxContext).context; +export const useTab = (): ToolboxActionConfig | undefined => useContext(ToolboxContext).activeTabBar; +export const useTabBarOpen = (): ((actionId: string, context?: string) => void) => useContext(ToolboxContext).open; +export const useTabBarClose = (): (() => void) => useContext(ToolboxContext).close; +export const useTabBarOpenUserInfo = (): ((username: string) => void) => useContext(ToolboxContext).openRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/Apps/AppsWithData.tsx b/apps/meteor/client/views/room/contextualBar/Apps/AppsWithData.tsx index bea01b72c0f..1cf6c4e50ac 100644 --- a/apps/meteor/client/views/room/contextualBar/Apps/AppsWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/Apps/AppsWithData.tsx @@ -15,7 +15,7 @@ import { kitContext } from '@rocket.chat/fuselage-ui-kit'; import React, { memo, useState, useEffect, useReducer, Dispatch, SyntheticEvent, ContextType } from 'react'; import { triggerBlockAction, triggerCancel, triggerSubmitView, on, off } from '../../../../../app/ui-message/client/ActionManager'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import Apps from './Apps'; type FieldStateValue = string | Array<string> | undefined; diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx index d159bc219e9..a08a154515d 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useEffect, useState, memo, ReactElement } from 'react'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointActionExperimental'; import { useEndpointData } from '../../../../hooks/useEndpointData'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import AutoTranslate from './AutoTranslate'; const AutoTranslateWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/withData.js b/apps/meteor/client/views/room/contextualBar/Discussions/withData.js index 4f61595d665..750f10fa304 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/withData.js +++ b/apps/meteor/client/views/room/contextualBar/Discussions/withData.js @@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import { useDiscussionsList } from './useDiscussionsList'; const subscriptionFields = { tunread: 1, tunreadUser: 1, tunreadGroup: 1 }; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index 194d2d882c4..4f2be9676f3 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState, useMemo, FC } from 'react'; import VerticalBar from '../../../../components/VerticalBar'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import FileExport from './FileExport'; import MailExportForm from './MailExportForm'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js index 1b33a42a3b1..24f9772cdb5 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js @@ -1,7 +1,7 @@ import { useUserRoom } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../../contexts/ToolboxContext'; import EditChannel from './EditChannel'; function EditChannelWithData({ rid, onClickBack }) { diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js index 855cdd59fe2..592352e7a03 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js @@ -21,7 +21,7 @@ import { useEndpointActionExperimental } from '../../../../../hooks/useEndpointA import * as Federation from '../../../../../lib/federation/Federation'; import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; import WarningModal from '../../../../admin/apps/WarningModal'; -import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../../contexts/ToolboxContext'; import ChannelToTeamModal from '../ChannelToTeamModal/ChannelToTeamModal'; import RoomInfo from './RoomInfo'; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx index afee820bb10..ffa8c022c5c 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcutsWithData.tsx @@ -1,7 +1,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { ReactElement } from 'react'; -import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; +import { ToolboxContextValue } from '../../contexts/ToolboxContext'; import KeyboardShortcuts from './KeyboardShortcuts'; const KeyboardShortcutsWithData = ({ tabBar }: { tabBar: ToolboxContextValue['tabBar'] }): ReactElement => { diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx index c3e402a31b1..1ab4274ab86 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx @@ -4,7 +4,7 @@ import React, { memo, ReactElement } from 'react'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointActionExperimental'; import { useForm } from '../../../../hooks/useForm'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import NotificationPreferences from './NotificationPreferences'; export type NotificationFormValues = { diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx index 68faa2e3c69..f7a1f64c548 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx @@ -5,7 +5,7 @@ import ORTInstance from '../../../../../app/otr/client/OTR'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; import { usePresence } from '../../../../hooks/usePresence'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import OTR from './OTR'; const OTRWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx index c37723cdf92..7a96680bad2 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useEffect, useState, ReactElement } from 'react'; import GenericModal from '../../../../components/GenericModal'; import { useForm } from '../../../../hooks/useForm'; -import { ToolboxContextValue } from '../../lib/Toolbox/ToolboxContext'; +import { ToolboxContextValue } from '../../contexts/ToolboxContext'; import PruneMessages from './PruneMessages'; const getTimeZoneOffset = (): string => { diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFilesWithData.js b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFilesWithData.js index e8331de6088..4f3d4ccd263 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFilesWithData.js +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFilesWithData.js @@ -5,7 +5,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import GenericModal from '../../../../components/GenericModal'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import RoomFiles from './RoomFiles'; import { useFilesList } from './hooks/useFilesList'; import { useMessageDeletionIsAllowed } from './hooks/useMessageDeletionIsAllowed'; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsersWithData.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsersWithData.tsx index 90987e94766..789d8387fe7 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsersWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsersWithData.tsx @@ -5,7 +5,7 @@ import React, { ReactElement } from 'react'; import { useForm } from '../../../../../hooks/useForm'; import { useRoom } from '../../../contexts/RoomContext'; -import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../../contexts/ToolboxContext'; import AddUsers from './AddUsers'; type AddUsersWithDataProps = { diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx index 0e56fafba45..6b82b1bacc5 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useTranslation, useToastMessageDispatch } from '@rocket.ch import React, { useState, useEffect, ReactElement } from 'react'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; -import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../../contexts/ToolboxContext'; import InviteUsers from './InviteUsers'; type InviteUsersWithDataProps = { diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx index 67d48941575..f2ef672596e 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx @@ -7,7 +7,7 @@ import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import * as Federation from '../../../../lib/federation/Federation'; import { useMembersList } from '../../../hooks/useMembersList'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import UserInfoWithData from '../UserInfo'; import AddUsers from './AddUsers'; import InviteUsers from './InviteUsers'; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx index 56202b8e5ae..9ff163b9070 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx @@ -7,7 +7,7 @@ import { Virtuoso } from 'react-virtuoso'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; import VerticalBar from '../../../../components/VerticalBar'; -import { useTabContext } from '../../providers/ToolboxProvider'; +import { useTabContext } from '../../contexts/ToolboxContext'; import ThreadComponent from '../../threads/ThreadComponent'; import ThreadRow from './ThreadRow'; import { withData } from './withData'; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/withData.tsx b/apps/meteor/client/views/room/contextualBar/Threads/withData.tsx index c1c3cca8060..625f0f7ab77 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/withData.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/withData.tsx @@ -5,7 +5,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import { ThreadsListOptions } from '../../../../lib/lists/ThreadsList'; -import { useTabBarClose } from '../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; import { ThreadListProps } from './ThreadList'; import { useThreadsList } from './useThreadsList'; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx index 09ac842cf30..76592499076 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx @@ -3,7 +3,7 @@ import React, { ReactElement, useMemo } from 'react'; import { useRecordList } from '../../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../../hooks/useAsyncState'; -import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useTabBarClose } from '../../../contexts/ToolboxContext'; import VideoConfList from './VideoConfList'; import { useVideoConfList } from './useVideoConfList'; diff --git a/apps/meteor/client/views/room/lib/Toolbox/ToolboxContext.tsx b/apps/meteor/client/views/room/lib/Toolbox/ToolboxContext.tsx deleted file mode 100644 index 752f015108d..00000000000 --- a/apps/meteor/client/views/room/lib/Toolbox/ToolboxContext.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable react/no-multi-comp */ -import { IRoom } from '@rocket.chat/core-typings'; -import { EventHandlerOf } from '@rocket.chat/emitter'; -import { createContext, useContext } from 'react'; - -import { actions, listen, ToolboxActionConfig, ToolboxAction, Events } from '.'; -import './defaultActions'; - -export type ToolboxEventHandler = (handler: EventHandlerOf<Events, 'change'>) => () => void; - -export type ToolboxContextValue = { - actions: Map<ToolboxActionConfig['id'], ToolboxAction>; - listen: ToolboxEventHandler; - tabBar?: any; - context?: any; - open: (actionId: string, context?: string) => void; - openUserInfo: (username: string) => void; - close: () => void; - activeTabBar?: ToolboxActionConfig; - setData?: (data: Record<string, unknown>) => void; -}; - -export const ToolboxContext = createContext<ToolboxContextValue>({ - actions, - listen, - open: () => null, - openUserInfo: () => null, - close: () => null, -}); - -export const useToolboxContext = (): ToolboxContextValue => useContext(ToolboxContext); - -/* - * @deprecated - * we cannot reach this context because the messages are wrapped by blaze - */ - -const tabBarStore = new Map<IRoom['_id'], ToolboxContextValue>(); - -/* - * @deprecated - * we cannot reach this context because the messages are wrapped by blaze - */ -export const getTabBarContext = (rid: IRoom['_id']): ToolboxContextValue => { - const result = tabBarStore.get(rid); - if (!result) { - throw new Error('TabBar context not found'); - } - return result; -}; - -export const setTabBarContext = (rid: IRoom['_id'], context: ToolboxContextValue): void => { - tabBarStore.set(rid, context); -}; - -export const removeTabBarContext = (rid: IRoom['_id']): void => { - tabBarStore.delete(rid); -}; diff --git a/apps/meteor/client/views/room/lib/Toolbox/index.tsx b/apps/meteor/client/views/room/lib/Toolbox/index.tsx index 9074c5749fd..ecbafa7ad45 100644 --- a/apps/meteor/client/views/room/lib/Toolbox/index.tsx +++ b/apps/meteor/client/views/room/lib/Toolbox/index.tsx @@ -3,7 +3,7 @@ import { Box, Option, Icon } from '@rocket.chat/fuselage'; import { TranslationKey } from '@rocket.chat/ui-contexts'; import { ReactNode, MouseEvent, ComponentProps, ComponentType } from 'react'; -import { ToolboxContextValue } from './ToolboxContext'; +import { ToolboxContextValue } from '../../contexts/ToolboxContext'; import { generator, Events as GeneratorEvents } from './generator'; type ToolboxHook = ({ room }: { room: IRoom }) => ToolboxActionConfig | null; diff --git a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx index 88daccc79b8..b67c5aadbc1 100644 --- a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx +++ b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx @@ -5,14 +5,13 @@ import React, { ReactElement } from 'react'; import { openUserCard } from '../../../../../app/ui/client/lib/UserCard'; import GenericModal from '../../../../components/GenericModal'; +import { ToolboxContextValue } from '../../contexts/ToolboxContext'; import Reactions from './Reactions'; type ReactionListProps = { rid: string; reactions: Required<IMessage>['reactions']; - tabBar: { - openUserInfo: (username: string) => void; - }; + tabBar: ToolboxContextValue; onClose: () => void; }; @@ -33,7 +32,7 @@ const ReactionList = ({ rid, reactions, tabBar, onClose }: ReactionListProps): R open: (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); onClose(); - tabBar.openUserInfo(username); + tabBar.openRoomInfo(username); }, }); }); diff --git a/apps/meteor/client/views/room/providers/MessageProvider.tsx b/apps/meteor/client/views/room/providers/MessageProvider.tsx index 5281b7b21d0..96b2aec42d8 100644 --- a/apps/meteor/client/views/room/providers/MessageProvider.tsx +++ b/apps/meteor/client/views/room/providers/MessageProvider.tsx @@ -10,7 +10,7 @@ import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import { fireGlobalEvent } from '../../../lib/utils/fireGlobalEvent'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; import { MessageContext } from '../contexts/MessageContext'; -import { useTabBarOpen } from './ToolboxProvider'; +import { useTabBarOpen } from '../contexts/ToolboxContext'; export const MessageProvider = memo(function MessageProvider({ rid, diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index d4c1f7720fe..e2813389ce8 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,13 +1,16 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { useUserSubscription } from '@rocket.chat/ui-contexts'; -import React, { ReactNode, useContext, useMemo, memo, useEffect, useCallback } from 'react'; +import { IRoom } from '@rocket.chat/core-typings'; +import React, { ReactNode, useMemo, memo, useEffect, ContextType, ReactElement, useCallback } from 'react'; import { UserAction } from '../../../../app/ui'; -import { RoomManager, useHandleRoom } from '../../../lib/RoomManager'; -import { AsyncStatePhase } from '../../../lib/asyncState'; +import { RoomHistoryManager } from '../../../../app/ui-utils/client'; +import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; +import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { RoomManager } from '../../../lib/RoomManager'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import RoomSkeleton from '../RoomSkeleton'; -import { RoomContext, RoomContextValue } from '../contexts/RoomContext'; +import { useRoomRolesManagement } from '../components/body/useRoomRolesManagement'; +import { RoomAPIContext } from '../contexts/RoomAPIContext'; +import { RoomContext } from '../contexts/RoomContext'; import ToolboxProvider from './ToolboxProvider'; type RoomProviderProps = { @@ -15,28 +18,50 @@ type RoomProviderProps = { rid: IRoom['_id']; }; -const fields = {}; +const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { + useRoomRolesManagement(rid); -const RoomProvider = ({ rid, children }: RoomProviderProps): JSX.Element => { - const { phase, value: room } = useHandleRoom(rid); + const roomQuery = useReactiveQuery(['rooms', rid], ({ rooms }) => rooms.findOne({ _id: rid })); + const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], ({ subscriptions }) => subscriptions.findOne({ rid })); - const getMore = useCallback(() => { - RoomManager.getMore(rid); - }, [rid]); + const pseudoRoom = useMemo(() => { + if (!roomQuery.data) { + return null; + } + + return { + ...subscriptionQuery.data, + ...roomQuery.data, + name: roomCoordinator.getRoomName(roomQuery.data.t, roomQuery.data), + }; + }, [roomQuery.data, subscriptionQuery.data]); + + const { hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages } = useReactiveValue( + useCallback(() => { + const { hasMore, hasMoreNext, isLoading } = RoomHistoryManager.getRoom(rid); + + return { + hasMorePreviousMessages: hasMore.get(), + hasMoreNextMessages: hasMoreNext.get(), + isLoadingMoreMessages: isLoading.get(), + }; + }, [rid]), + ); - const subscribed = Boolean(useUserSubscription(rid, fields)); - const context = useMemo(() => { - if (!room) { + const context = useMemo((): ContextType<typeof RoomContext> => { + if (!pseudoRoom) { return null; } - room._id = rid; + return { - subscribed, rid, - getMore, - room: { ...room, name: roomCoordinator.getRoomName(room.t, room) }, + room: pseudoRoom, + subscription: subscriptionQuery.data, + hasMorePreviousMessages, + hasMoreNextMessages, + isLoadingMoreMessages, }; - }, [room, rid, subscribed, getMore]); + }, [hasMoreNextMessages, hasMorePreviousMessages, isLoadingMoreMessages, pseudoRoom, rid, subscriptionQuery.data]); useEffect(() => { RoomManager.open(rid); @@ -46,41 +71,29 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): JSX.Element => { }, [rid]); useEffect(() => { - if (!subscribed) { - return (): void => undefined; + if (!subscriptionQuery.data) { + return; } UserAction.addStream(rid); return (): void => { UserAction.cancel(rid); }; - }, [rid, subscribed]); + }, [rid, subscriptionQuery.data]); + + const api = useMemo(() => ({}), []); - if (phase === AsyncStatePhase.LOADING || !room) { + if (!pseudoRoom) { return <RoomSkeleton />; } return ( - <RoomContext.Provider value={context}> - <ToolboxProvider room={room}>{children}</ToolboxProvider> - </RoomContext.Provider> + <RoomAPIContext.Provider value={api}> + <RoomContext.Provider value={context}> + <ToolboxProvider room={pseudoRoom}>{children}</ToolboxProvider> + </RoomContext.Provider> + </RoomAPIContext.Provider> ); }; -export const useRoom = (): IRoom => { - const context = useContext(RoomContext); - if (!context) { - throw Error('useRoom should be used only inside rooms context'); - } - return context.room; -}; - -export const useRoomContext = (): RoomContextValue => { - const context = useContext(RoomContext); - if (!context) { - throw Error('useRoom should be used only inside rooms context'); - } - return context; -}; - export default memo(RoomProvider); diff --git a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx index d55344d711f..48a3fdeaf4a 100644 --- a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx @@ -1,15 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useDebouncedState, useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks'; -import { useSession, useCurrentRoute, useRoute, useUserId, useSetting } from '@rocket.chat/ui-contexts'; -import React, { ReactNode, useContext, useMemo, useState, useLayoutEffect, useEffect } from 'react'; +import { useCurrentRoute, useRoute, useUserId, useSetting } from '@rocket.chat/ui-contexts'; +import React, { ReactNode, useContext, useMemo, useState, useLayoutEffect } from 'react'; -import { - removeTabBarContext, - setTabBarContext, - ToolboxContext, - ToolboxContextValue, - ToolboxEventHandler, -} from '../lib/Toolbox/ToolboxContext'; +import { ToolboxContext, ToolboxContextValue, ToolboxEventHandler } from '../contexts/ToolboxContext'; import { Store } from '../lib/Toolbox/generator'; import { ToolboxAction, ToolboxActionConfig } from '../lib/Toolbox/index'; import VirtualAction from './VirtualAction'; @@ -33,8 +27,7 @@ const useToolboxActions = (room: IRoom): { listen: ToolboxEventHandler; actions: const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom }): JSX.Element => { const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); const uid = useUserId(); - const [activeTabBar, setActiveTabBar] = useState<[ToolboxActionConfig | undefined, string?]>([undefined]); - const [list, setList] = useSafely(useDebouncedState<Store<ToolboxAction>>(new Map(), 5)); + const [list, setList] = useSafely(useDebouncedState<Store<ToolboxAction>>(new Map<string, ToolboxActionConfig>(), 5)); const handleChange = useMutableCallback((fn) => { fn(list); setList((list) => new Map(list)); @@ -44,11 +37,14 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom const [routeName, params] = useCurrentRoute(); const router = useRoute(routeName || ''); - const currentRoom = useSession('openedRoom'); - const tab = params?.tab; const context = params?.context; + const activeTabBar = useMemo( + (): [ToolboxActionConfig | undefined, string?] => [tab ? (list.get(tab) as ToolboxActionConfig) : undefined, context], + [tab, list, context], + ); + const close = useMutableCallback(() => { router.push({ ...params, @@ -69,7 +65,7 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom }); }); - const openUserInfo = useMutableCallback((username) => { + const openRoomInfo = useMutableCallback((username?: string) => { switch (room.t) { case 'l': open('room-info', username); @@ -83,36 +79,19 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom } }); - useLayoutEffect(() => { - if (!tab) { - setActiveTabBar([undefined, undefined]); - } - - setActiveTabBar([list.get(tab as string) as ToolboxActionConfig, context]); - }, [tab, list, currentRoom, context]); - - // TODO: make the object stable, preventing re-renders const contextValue = useMemo( - () => ({ + (): ToolboxContextValue => ({ listen, actions: new Map(list), activeTabBar: activeTabBar[0], context: activeTabBar[1], open, close, - openUserInfo, + openRoomInfo, }), - [listen, list, activeTabBar, open, close, openUserInfo], + [listen, list, activeTabBar, open, close, openRoomInfo], ); - // TODO: remove this when the messages are running on react diretly, not wrapped by blaze - useEffect(() => { - setTabBarContext(room._id, contextValue); - return (): void => { - removeTabBarContext(room._id); - }; - }, [contextValue, room._id]); - return ( <ToolboxContext.Provider value={contextValue}> {actions @@ -127,11 +106,4 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom ); }; -export const useTabContext = (): unknown | undefined => useContext(ToolboxContext).context; -export const useTab = (): ToolboxActionConfig | undefined => useContext(ToolboxContext).activeTabBar; -export const useTabBarOpen = (): ((actionId: string, context?: string) => void) => useContext(ToolboxContext).open; -export const useTabBarClose = (): (() => void) => useContext(ToolboxContext).close; -export const useTabBarOpenUserInfo = (): ((username: string) => void) => useContext(ToolboxContext).openUserInfo; -export const useTabBarAPI = (): ToolboxContextValue => useContext(ToolboxContext); - export default ToolboxProvider; diff --git a/apps/meteor/client/views/room/threads/ThreadComponent.tsx b/apps/meteor/client/views/room/threads/ThreadComponent.tsx index 908d760d011..be4f9f8fec5 100644 --- a/apps/meteor/client/views/room/threads/ThreadComponent.tsx +++ b/apps/meteor/client/views/room/threads/ThreadComponent.tsx @@ -10,7 +10,7 @@ import { ChatMessage } from '../../../../app/models/client'; import { normalizeThreadTitle } from '../../../../app/threads/client/lib/normalizeThreadTitle'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi'; -import { useTabBarOpenUserInfo } from '../providers/ToolboxProvider'; +import { useTabBarOpenUserInfo } from '../contexts/ToolboxContext'; import ThreadSkeleton from './ThreadSkeleton'; import ThreadView from './ThreadView'; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js index 43e8160eab6..fd79b4e745b 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js @@ -6,8 +6,8 @@ import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../lib/asyncState'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import CreateChannelWithData from '../../../../sidebar/header/CreateChannelWithData'; +import { useTabBarClose } from '../../../room/contexts/ToolboxContext'; import RoomInfo from '../../../room/contextualBar/Info'; -import { useTabBarClose } from '../../../room/providers/ToolboxProvider'; import AddExistingModal from './AddExistingModal'; import BaseTeamsChannels from './BaseTeamsChannels'; import { useTeamsChannelList } from './hooks/useTeamsChannelList'; diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js index c49d310543e..f3fcf41f8e9 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js @@ -17,7 +17,7 @@ import MarkdownText from '../../../../components/MarkdownText'; import { useDontAskAgain } from '../../../../hooks/useDontAskAgain'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointActionExperimental'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import { useTabBarClose, useTabBarOpen } from '../../../room/providers/ToolboxProvider'; +import { useTabBarClose, useTabBarOpen } from '../../../room/contexts/ToolboxContext'; import ConvertToChannelModal from '../../ConvertToChannelModal'; import DeleteTeamModal from './Delete'; import LeaveTeamModal from './Leave'; diff --git a/apps/meteor/definition/IRoomTypeConfig.ts b/apps/meteor/definition/IRoomTypeConfig.ts index d039acacd60..1819e48e68c 100644 --- a/apps/meteor/definition/IRoomTypeConfig.ts +++ b/apps/meteor/definition/IRoomTypeConfig.ts @@ -66,7 +66,6 @@ export interface IRoomTypeClientDirectives { allowMemberAction: (room: Partial<IRoom>, action: ValueOf<typeof RoomMemberActions>) => boolean; roomName: (room: AtLeast<IRoom, '_id' | 'name' | 'fname' | 'prid'>) => string | undefined; isGroupChat: (room: Partial<IRoom>) => boolean; - openCustomProfileTab: (instance: any, room: IRoom, username: string) => boolean; getUiText: (context: ValueOf<typeof UiTextContext>) => string; condition: () => boolean; getAvatarPath: ( diff --git a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx index 3a7b75a28e9..2edaef29d87 100644 --- a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx +++ b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx @@ -7,7 +7,7 @@ import { Virtuoso } from 'react-virtuoso'; import ScrollableContentWrapper from '../../../../../../client/components/ScrollableContentWrapper'; import VerticalBar from '../../../../../../client/components/VerticalBar'; -import { useTabContext } from '../../../../../../client/views/room/providers/ToolboxProvider'; +import { useTabContext } from '../../../../../../client/views/room/contexts/ToolboxContext'; import Item from './Item'; import WrapCannedResponse from './WrapCannedResponse'; diff --git a/packages/core-typings/src/ISubscription.ts b/packages/core-typings/src/ISubscription.ts index 7b140a775e3..cbe9e84f834 100644 --- a/packages/core-typings/src/ISubscription.ts +++ b/packages/core-typings/src/ISubscription.ts @@ -28,7 +28,7 @@ export interface ISubscription extends IRocketChatRecord { userMentions: number; groupMentions: number; - broadcast?: boolean; + broadcast?: true; tunread?: Array<string>; tunreadGroup?: Array<string>; tunreadUser?: Array<string>; @@ -45,14 +45,14 @@ export interface ISubscription extends IRocketChatRecord { fname?: string; code?: unknown; - archived?: unknown; + archived?: boolean; audioNotificationValue?: string; desktopNotifications?: 'all' | 'mentions' | 'nothing'; mobilePushNotifications?: 'all' | 'mentions' | 'nothing'; emailNotifications?: 'all' | 'mentions' | 'nothing'; blocked?: unknown; blocker?: unknown; - autoTranslate?: unknown; + autoTranslate?: boolean; autoTranslateLanguage?: string; disableNotifications?: boolean; muteGroupMentions?: boolean; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts index 457d645d757..d6a77b9d044 100644 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts @@ -5,9 +5,9 @@ import { useQueryClient } from '@tanstack/react-query'; import { useVideoConfData } from './useVideoConfData'; -const ee = new Emitter(); +const ee = new Emitter<Record<string, void>>(); -const events = new Map(); +const events = new Map<string, () => void>(); const useStreamBySubPath = ( streamer: ReturnType<typeof useStream>, @@ -18,8 +18,8 @@ const useStreamBySubPath = ( if (!ee.has(subpath)) { events.set( subpath, - streamer(subpath, (...args) => { - ee.emit(subpath, ...args); + streamer(subpath, () => { + ee.emit(subpath); }) ); } diff --git a/packages/ui-contexts/src/ServerContext/ServerContext.ts b/packages/ui-contexts/src/ServerContext/ServerContext.ts index 00df4161200..7297879f4b7 100644 --- a/packages/ui-contexts/src/ServerContext/ServerContext.ts +++ b/packages/ui-contexts/src/ServerContext/ServerContext.ts @@ -36,7 +36,7 @@ export type ServerContextValue = { retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, - ) => <T>(eventName: string, callback: (data: T) => void) => () => void; + ) => <TEvent extends unknown[]>(eventName: string, callback: (...event: TEvent) => void) => () => void; }; export const ServerContext = createContext<ServerContextValue>({ diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 5ba69c41bd9..8b297bd2a40 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -174,6 +174,7 @@ export interface ServerMethods { moreBefore: boolean; moreAfter: boolean; }; + 'logoutCleanUp': (user: IUser) => void; 'Mailer.sendMail': (from: string, subject: string, body: string, dryrun: boolean, query: string) => any; 'muteUserInRoom': (...args: any[]) => any; 'openRoom': (rid: IRoom['_id']) => ISubscription; diff --git a/packages/ui-contexts/src/hooks/useStream.ts b/packages/ui-contexts/src/hooks/useStream.ts index 7e9d0461667..91613e3fb8e 100644 --- a/packages/ui-contexts/src/hooks/useStream.ts +++ b/packages/ui-contexts/src/hooks/useStream.ts @@ -8,7 +8,7 @@ export const useStream = ( retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, -): (<T>(eventName: string, callback: (data: T) => void) => () => void) => { +): (<TEvent extends unknown[]>(eventName: string, callback: (...event: TEvent) => void) => () => void) => { const { getStream } = useContext(ServerContext); return useMemo(() => getStream(streamName, options), [getStream, streamName, options]); }; -- GitLab From 0beec2f87fa57ba164ddb3ccc55dcbdb7284c492 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 23 Sep 2022 15:36:10 -0300 Subject: [PATCH 068/107] Chore: Convert current-chats to useQuery (#26931) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .../currentChats/CurrentChatsRoute.tsx | 133 +++++++++--------- .../currentChats/RemoveChatButton.tsx | 16 +-- .../{ => hooks}/useAllCustomFields.tsx | 0 .../currentChats/hooks/useCurrentChats.ts | 9 ++ .../hooks/useRemoveCurrentChatMutation.ts | 18 +++ apps/meteor/definition/methods/omnichannel.ts | 3 +- 6 files changed, 101 insertions(+), 78 deletions(-) rename apps/meteor/client/views/omnichannel/currentChats/{ => hooks}/useAllCustomFields.tsx (100%) create mode 100644 apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts create mode 100644 apps/meteor/client/views/omnichannel/currentChats/hooks/useRemoveCurrentChatMutation.ts diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 29ef6bf6b76..d769c9e7ee0 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -17,14 +17,13 @@ import { GenericTable } from '../../../components/GenericTable/V2/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; import Page from '../../../components/Page'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import { AsyncStatePhase } from '../../../lib/asyncState'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import Chat from '../directory/chats/Chat'; import CustomFieldsVerticalBar from './CustomFieldsVerticalBar'; import FilterByText from './FilterByText'; import RemoveChatButton from './RemoveChatButton'; -import { useAllCustomFields } from './useAllCustomFields'; +import { useAllCustomFields } from './hooks/useAllCustomFields'; +import { useCurrentChats } from './hooks/useCurrentChats'; type useQueryType = ( debouncedParams: { @@ -41,74 +40,73 @@ type useQueryType = ( }, customFields: { [key: string]: string } | undefined, [column, direction]: [string, 'asc' | 'desc'], -) => GETLivechatRoomsParams | undefined; +) => GETLivechatRoomsParams; const sortDir = (sortDir: 'asc' | 'desc'): 1 | -1 => (sortDir === 'asc' ? 1 : -1); -const useQuery: useQueryType = ( +const currentChatQuery: useQueryType = ( { guest, servedBy, department, status, from, to, tags, itemsPerPage, current }, customFields, [column, direction], -) => - useMemo(() => { - const query: { - agents?: string[]; - offset?: number; - roomName?: string; - departmentId?: string; - open?: boolean; - createdAt?: string; - closedAt?: string; - tags?: string[]; - onhold?: boolean; - customFields?: string; - sort: string; - count?: number; - } = { - ...(guest && { roomName: guest }), - sort: JSON.stringify({ - [column]: sortDir(direction), - ts: column === 'ts' ? sortDir(direction) : undefined, - }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }; +) => { + const query: { + agents?: string[]; + offset?: number; + roomName?: string; + departmentId?: string; + open?: boolean; + createdAt?: string; + closedAt?: string; + tags?: string[]; + onhold?: boolean; + customFields?: string; + sort: string; + count?: number; + } = { + ...(guest && { roomName: guest }), + sort: JSON.stringify({ + [column]: sortDir(direction), + ts: column === 'ts' ? sortDir(direction) : undefined, + }), + ...(itemsPerPage && { count: itemsPerPage }), + ...(current && { offset: current }), + }; - if (from || to) { - query.createdAt = JSON.stringify({ - ...(from && { - start: moment(new Date(from)).set({ hour: 0, minutes: 0, seconds: 0 }).format('YYYY-MM-DDTHH:mm:ss'), - }), - ...(to && { - end: moment(new Date(to)).set({ hour: 23, minutes: 59, seconds: 59 }).format('YYYY-MM-DDTHH:mm:ss'), - }), - }); - } + if (from || to) { + query.createdAt = JSON.stringify({ + ...(from && { + start: moment(new Date(from)).set({ hour: 0, minutes: 0, seconds: 0 }).format('YYYY-MM-DDTHH:mm:ss'), + }), + ...(to && { + end: moment(new Date(to)).set({ hour: 23, minutes: 59, seconds: 59 }).format('YYYY-MM-DDTHH:mm:ss'), + }), + }); + } - if (status !== 'all') { - query.open = status === 'opened' || status === 'onhold'; - query.onhold = status === 'onhold'; - } - if (servedBy && servedBy !== 'all') { - query.agents = [servedBy]; - } - if (department && department !== 'all') { - query.departmentId = department; - } + if (status !== 'all') { + query.open = status === 'opened' || status === 'onhold'; + query.onhold = status === 'onhold'; + } + if (servedBy && servedBy !== 'all') { + query.agents = [servedBy]; + } + if (department && department !== 'all') { + query.departmentId = department; + } - if (tags && tags.length > 0) { - query.tags = tags; - } + if (tags && tags.length > 0) { + query.tags = tags; + } - if (customFields && Object.keys(customFields).length > 0) { - const customFieldsQuery = Object.fromEntries(Object.entries(customFields).filter((item) => item[1] !== undefined && item[1] !== '')); - if (Object.keys(customFieldsQuery).length > 0) { - query.customFields = JSON.stringify(customFieldsQuery); - } + if (customFields && Object.keys(customFields).length > 0) { + const customFieldsQuery = Object.fromEntries(Object.entries(customFields).filter((item) => item[1] !== undefined && item[1] !== '')); + if (Object.keys(customFieldsQuery).length > 0) { + query.customFields = JSON.stringify(customFieldsQuery); } + } - return query; - }, [guest, column, direction, itemsPerPage, current, from, to, status, servedBy, department, tags, customFields]); + return query; +}; const CurrentChatsRoute = (): ReactElement => { const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open'>('ts', 'desc'); @@ -136,12 +134,13 @@ const CurrentChatsRoute = (): ReactElement => { 500, ) as ['fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open', 'asc' | 'desc']; - const query = useQuery(debouncedParams, debouncedCustomFields, debouncedSort); + const query = currentChatQuery(debouncedParams, debouncedCustomFields, debouncedSort); const canViewCurrentChats = usePermission('view-livechat-current-chats'); const canRemoveClosedChats = usePermission('remove-closed-livechat-room'); const directoryRoute = useRoute('omnichannel-current-chats'); - const { reload, ...result } = useEndpointData('/v1/livechat/rooms', query); + const result = useCurrentChats(query); + const { data: allCustomFields } = useAllCustomFields(); const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); @@ -184,13 +183,13 @@ const CurrentChatsRoute = (): ReactElement => { </GenericTableCell> {canRemoveClosedChats && !open && ( <GenericTableCell withTruncatedText> - <RemoveChatButton _id={_id} reload={reload} /> + <RemoveChatButton _id={_id} /> </GenericTableCell> )} </GenericTableRow> ); }, - [canRemoveClosedChats, onRowClick, reload, t], + [canRemoveClosedChats, onRowClick, t], ); if (!canViewCurrentChats) { @@ -282,15 +281,15 @@ const CurrentChatsRoute = (): ReactElement => { )} </GenericTableHeader> <GenericTableBody data-qa='GenericTableCurrentChatsBody'> - {result.phase === AsyncStatePhase.LOADING && <GenericTableLoadingTable headerCells={4} />} - {result.phase === AsyncStatePhase.RESOLVED && result.value.rooms.map((room) => renderRow({ ...room }))} + {result.isLoading && <GenericTableLoadingTable headerCells={4} />} + {result.isSuccess && result.data.rooms.map((room) => renderRow({ ...room }))} </GenericTableBody> </GenericTable> - {result.phase === AsyncStatePhase.RESOLVED && ( + {result.isSuccess && ( <Pagination current={current} itemsPerPage={itemsPerPage} - count={result.value.total} + count={result.data.total} onSetItemsPerPage={setItemsPerPage} onSetCurrent={setCurrent} {...paginationProps} diff --git a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx index 21f0cbf168e..ffe23e08ba1 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx @@ -1,23 +1,19 @@ import { Table, IconButton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; import GenericModal from '../../../components/GenericModal'; +import { useRemoveCurrentChatMutation } from './hooks/useRemoveCurrentChatMutation'; -const RemoveChatButton: FC<{ _id: string; reload: () => void }> = ({ _id, reload }) => { - const removeChat = useMethod('livechat:removeRoom'); +const RemoveChatButton: FC<{ _id: string }> = ({ _id }) => { + const removeCurrentChatMutation = useRemoveCurrentChatMutation(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const t = useTranslation(); const handleRemoveClick = useMutableCallback(async () => { - try { - await removeChat(_id); - } catch (error) { - console.log(error); - } - reload(); + removeCurrentChatMutation.mutate(_id); }); const handleDelete = useMutableCallback((e) => { @@ -43,7 +39,7 @@ const RemoveChatButton: FC<{ _id: string; reload: () => void }> = ({ _id, reload return ( <Table.Cell fontScale='p2' color='hint' withTruncatedText data-qa='current-chats-cell-delete'> - <IconButton small icon='trash' title={t('Remove')} onClick={handleDelete} /> + <IconButton small icon='trash' title={t('Remove')} disabled={removeCurrentChatMutation.isLoading} onClick={handleDelete} /> </Table.Cell> ); }; diff --git a/apps/meteor/client/views/omnichannel/currentChats/useAllCustomFields.tsx b/apps/meteor/client/views/omnichannel/currentChats/hooks/useAllCustomFields.tsx similarity index 100% rename from apps/meteor/client/views/omnichannel/currentChats/useAllCustomFields.tsx rename to apps/meteor/client/views/omnichannel/currentChats/hooks/useAllCustomFields.tsx diff --git a/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts b/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts new file mode 100644 index 00000000000..741d9325bc6 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts @@ -0,0 +1,9 @@ +import type { GETLivechatRoomsParams, OperationResult } from '@rocket.chat/rest-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; + +export const useCurrentChats = (query: GETLivechatRoomsParams): UseQueryResult<OperationResult<'GET', '/v1/livechat/rooms'>> => { + const currentChats = useEndpoint('GET', '/v1/livechat/rooms'); + + return useQuery(['current-chats', query], () => currentChats(query)); +}; diff --git a/apps/meteor/client/views/omnichannel/currentChats/hooks/useRemoveCurrentChatMutation.ts b/apps/meteor/client/views/omnichannel/currentChats/hooks/useRemoveCurrentChatMutation.ts new file mode 100644 index 00000000000..2e12c2b7426 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/currentChats/hooks/useRemoveCurrentChatMutation.ts @@ -0,0 +1,18 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useMethod } from '@rocket.chat/ui-contexts'; +import { useQueryClient, useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; + +export const useRemoveCurrentChatMutation = ( + options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>, +): UseMutationResult<void, Error, IRoom['_id']> => { + const removeRoom = useMethod('livechat:removeRoom'); + const queryClient = useQueryClient(); + + return useMutation((rid) => removeRoom(rid), { + ...options, + onSuccess: (...args) => { + queryClient.invalidateQueries(['current-chats']); + options?.onSuccess?.(...args); + }, + }); +}; diff --git a/apps/meteor/definition/methods/omnichannel.ts b/apps/meteor/definition/methods/omnichannel.ts index 66039f02119..6207de603d1 100644 --- a/apps/meteor/definition/methods/omnichannel.ts +++ b/apps/meteor/definition/methods/omnichannel.ts @@ -1,3 +1,4 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import '@rocket.chat/ui-contexts'; declare module '@rocket.chat/ui-contexts' { @@ -30,7 +31,7 @@ declare module '@rocket.chat/ui-contexts' { 'livechat:removeCustomField': (...args: any[]) => any; 'livechat:removeMonitor': (...args: any[]) => any; 'livechat:removePriority': (...args: any[]) => any; - 'livechat:removeRoom': (...args: any[]) => any; + 'livechat:removeRoom': (rid: IRoom['_id']) => void; 'livechat:removeTag': (...args: any[]) => any; 'livechat:removeTrigger': (...args: any[]) => any; 'livechat:removeUnit': (...args: any[]) => any; -- GitLab From 7407f626d6be13894a9b7dcb483e7f7c8ccf5372 Mon Sep 17 00:00:00 2001 From: Allan RIbeiro <35040806+AllanPazRibeiro@users.noreply.github.com> Date: Fri, 23 Sep 2022 16:57:47 -0300 Subject: [PATCH 069/107] Chore: Updating apps engine (#26924) --- apps/meteor/package.json | 2 +- yarn.lock | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cd04236706c..54a22b14c80 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -198,7 +198,7 @@ "@nivo/pie": "0.79.1", "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", - "@rocket.chat/apps-engine": "1.34.0", + "@rocket.chat/apps-engine": "1.35.0-alpha.46", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", diff --git a/yarn.lock b/yarn.lock index 61d4c6be812..d2f3839e177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5532,9 +5532,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.34.0": - version: 1.34.0 - resolution: "@rocket.chat/apps-engine@npm:1.34.0" +"@rocket.chat/apps-engine@npm:1.35.0-alpha.46": + version: 1.35.0-alpha.46 + resolution: "@rocket.chat/apps-engine@npm:1.35.0-alpha.46" dependencies: adm-zip: ^0.5.9 cryptiles: ^4.1.3 @@ -5542,7 +5542,8 @@ __metadata: semver: ^5.7.1 stack-trace: 0.0.10 uuid: ^3.4.0 - checksum: e6bb12feb78d6f8a054a4197130dd8335eceb3885a801b21b833dca46c40af682a29d19bb4dc67b62d8f07a3946e999eb50a7c724cafc149715de825c93e1ca4 + vm2: ^3.9.11 + checksum: 82376bf2765f2a7b548f9a30fcd4120c22e41b0605b3b74cf8a9cb685e08b8d55e1490cf736a45a6d780957e58a24e90234408124fef5badcadf9989b9a1c306 languageName: node linkType: hard @@ -6137,7 +6138,7 @@ __metadata: "@playwright/test": ^1.22.2 "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" - "@rocket.chat/apps-engine": 1.34.0 + "@rocket.chat/apps-engine": 1.35.0-alpha.46 "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next @@ -36065,7 +36066,7 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.10": +"vm2@npm:^3.9.10, vm2@npm:^3.9.11": version: 3.9.11 resolution: "vm2@npm:3.9.11" dependencies: -- GitLab From 426c7ed3f87b1a0b81ebbeaec603e3ba36d0b794 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 23 Sep 2022 17:15:38 -0300 Subject: [PATCH 070/107] Regression: Fix open room from current chats (#26930) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../app/ui-utils/client/lib/openRoom.tsx | 11 +++------- .../lib/createReactiveSubscriptionFactory.ts | 10 ++++----- .../currentChats/CurrentChatsRoute.tsx | 18 +++++----------- .../omnichannel/currentChats/FilterByText.tsx | 7 ++++--- .../currentChats/hooks/useCurrentChats.ts | 7 ++++++- .../views/room/providers/ToolboxProvider.tsx | 21 +++---------------- .../room/providers/hooks/useToolboxActions.ts | 21 +++++++++++++++++++ 7 files changed, 46 insertions(+), 49 deletions(-) create mode 100644 apps/meteor/client/views/room/providers/hooks/useToolboxActions.ts diff --git a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx index 62d52b2f379..5d54f54c890 100644 --- a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx +++ b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx @@ -13,7 +13,6 @@ import { callbacks } from '../../../../lib/callbacks'; import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { call } from '../../../../client/lib/utils/call'; import { RoomManager, RoomHistoryManager } from '..'; -import { RoomManager as NewRoomManager } from '../../../../client/lib/RoomManager'; import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import MainLayout from '../../../../client/views/root/MainLayout'; @@ -40,12 +39,13 @@ export async function openRoom(type: RoomType, name: string, render = true) { return FlowRouter.go('direct', { rid: room._id }, FlowRouter.current().queryParams); } + RoomManager.open({ typeName: type + name, rid: room._id }); + + c.stop(); if (room._id === Session.get('openedRoom') && !FlowRouter.getQueryParam('msg')) { return; } - RoomManager.open({ typeName: type + name, rid: room._id }); - if (render) { appLayout.render( <MainLayout> @@ -58,15 +58,10 @@ export async function openRoom(type: RoomType, name: string, render = true) { ); } - c.stop(); - if (RoomManager.currentTracker) { RoomManager.currentTracker = undefined; } - NewRoomManager.open(room._id); - Session.set('openedRoom', room._id); - fireGlobalEvent('room-opened', omit(room, 'usernames')); Session.set('editRoomTitle', false); diff --git a/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts index 0b7f19e0359..cc20c459571 100644 --- a/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts +++ b/apps/meteor/client/lib/createReactiveSubscriptionFactory.ts @@ -15,18 +15,16 @@ export const createReactiveSubscriptionFactory = const reactiveFn = (): void => { currentValue = computeCurrentValueWith(...args); - callbacks.forEach((callback) => { - queueMicrotask(callback); + queueMicrotask(() => { + callbacks.forEach((callback) => { + callback(); + }); }); }; let computation: Tracker.Computation | undefined; queueMicrotask(() => { - if (Tracker.currentComputation) { - throw new Error('Cannot call createReactiveSubscriptionFactory inside a Tracker computation'); - } - computation = Tracker.autorun(reactiveFn); }); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index d769c9e7ee0..1510c1c757d 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -1,9 +1,9 @@ import { Box, Pagination } from '@rocket.chat/fuselage'; -import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { GETLivechatRoomsParams } from '@rocket.chat/rest-typings'; import { usePermission, useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import moment from 'moment'; -import React, { memo, ReactElement, useCallback, useMemo, useState } from 'react'; +import React, { ComponentProps, memo, ReactElement, useCallback, useMemo, useState } from 'react'; import { GenericTableBody, @@ -115,7 +115,7 @@ const CurrentChatsRoute = (): ReactElement => { guest: '', fname: '', servedBy: '', - status: '', + status: 'all', department: '', from: '', to: '', @@ -126,15 +126,7 @@ const CurrentChatsRoute = (): ReactElement => { const t = useTranslation(); const id = useRouteParameter('id'); - const debouncedParams = useDebouncedValue(params, 500); - const debouncedCustomFields = useDebouncedValue(customFields, 500); - - const debouncedSort = useDebouncedValue( - useMemo(() => [sortBy, sortDirection], [sortBy, sortDirection]), - 500, - ) as ['fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open', 'asc' | 'desc']; - - const query = currentChatQuery(debouncedParams, debouncedCustomFields, debouncedSort); + const query = currentChatQuery(params, customFields, [sortBy, sortDirection]); const canViewCurrentChats = usePermission('view-livechat-current-chats'); const canRemoveClosedChats = usePermission('remove-closed-livechat-room'); const directoryRoute = useRoute('omnichannel-current-chats'); @@ -204,7 +196,7 @@ const CurrentChatsRoute = (): ReactElement => { <Page.Header title={t('Current_Chats')} /> <Box pi='24px'> <FilterByText - setFilter={setParams} + setFilter={setParams as ComponentProps<typeof FilterByText>['setFilter']} setCustomFields={setCustomFields} customFields={customFields} hasCustomFields={hasCustomFields} diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx index eaaa2cb4274..ba5f94b6560 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx @@ -12,7 +12,7 @@ import Label from './Label'; import RemoveAllClosed from './RemoveAllClosed'; type FilterByTextType = FC<{ - setFilter: Dispatch<SetStateAction<any>>; + setFilter: Dispatch<SetStateAction<Record<string, any>>>; setCustomFields: Dispatch<SetStateAction<{ [key: string]: string } | undefined>>; customFields: { [key: string]: string } | undefined; hasCustomFields: boolean; @@ -69,7 +69,8 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, customFields, setCu const onSubmit = useMutableCallback((e) => e.preventDefault()); useEffect(() => { - setFilter({ + setFilter((data) => ({ + ...data, guest, servedBy, status, @@ -78,7 +79,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, customFields, setCu to: to && moment(new Date(to)).utc().format('YYYY-MM-DDTHH:mm:ss'), tags: tags.map((tag) => tag.label), customFields, - }); + })); }, [setFilter, guest, servedBy, status, department, from, to, tags, customFields]); const handleClearFilters = useMutableCallback(() => { diff --git a/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts b/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts index 741d9325bc6..15e6606b41d 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts +++ b/apps/meteor/client/views/omnichannel/currentChats/hooks/useCurrentChats.ts @@ -1,3 +1,4 @@ +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { GETLivechatRoomsParams, OperationResult } from '@rocket.chat/rest-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; @@ -5,5 +6,9 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; export const useCurrentChats = (query: GETLivechatRoomsParams): UseQueryResult<OperationResult<'GET', '/v1/livechat/rooms'>> => { const currentChats = useEndpoint('GET', '/v1/livechat/rooms'); - return useQuery(['current-chats', query], () => currentChats(query)); + const debouncedQuery = useDebouncedValue(query, 500); + + return useQuery(['current-chats', debouncedQuery], () => currentChats(debouncedQuery), { + refetchOnMount: false, + }); }; diff --git a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx index 48a3fdeaf4a..e3cada3b60e 100644 --- a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx @@ -1,28 +1,13 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useDebouncedState, useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks'; import { useCurrentRoute, useRoute, useUserId, useSetting } from '@rocket.chat/ui-contexts'; -import React, { ReactNode, useContext, useMemo, useState, useLayoutEffect } from 'react'; +import React, { ReactNode, useMemo } from 'react'; -import { ToolboxContext, ToolboxContextValue, ToolboxEventHandler } from '../contexts/ToolboxContext'; +import { ToolboxContext, ToolboxContextValue } from '../contexts/ToolboxContext'; import { Store } from '../lib/Toolbox/generator'; import { ToolboxAction, ToolboxActionConfig } from '../lib/Toolbox/index'; import VirtualAction from './VirtualAction'; - -const useToolboxActions = (room: IRoom): { listen: ToolboxEventHandler; actions: Array<[string, ToolboxAction]> } => { - const { listen, actions } = useContext(ToolboxContext); - const [state, setState] = useState<Array<[string, ToolboxAction]>>(Array.from(actions.entries())); - - useLayoutEffect(() => { - const stop = listen((actions) => { - setState(Array.from(actions.entries())); - }); - return (): void => { - stop(); - }; - }, [listen, room, setState]); - - return { listen, actions: state }; -}; +import { useToolboxActions } from './hooks/useToolboxActions'; const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom }): JSX.Element => { const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); diff --git a/apps/meteor/client/views/room/providers/hooks/useToolboxActions.ts b/apps/meteor/client/views/room/providers/hooks/useToolboxActions.ts new file mode 100644 index 00000000000..a67e0492407 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useToolboxActions.ts @@ -0,0 +1,21 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { useContext, useState, useLayoutEffect } from 'react'; + +import { ToolboxContext, ToolboxEventHandler } from '../../contexts/ToolboxContext'; +import { ToolboxAction } from '../../lib/Toolbox/index'; + +export const useToolboxActions = (room: IRoom): { listen: ToolboxEventHandler; actions: Array<[string, ToolboxAction]> } => { + const { listen, actions } = useContext(ToolboxContext); + const [state, setState] = useSafely(useState<Array<[string, ToolboxAction]>>(Array.from(actions.entries()))); + + useLayoutEffect( + () => + listen((actions) => { + setState(Array.from(actions.entries())); + }), + [listen, room, setState], + ); + + return { listen, actions: state }; +}; -- GitLab From 5d3a3d24cbbd268892799aa30126fa0606f17170 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty <debdut.chakraborty@rocket.chat> Date: Sat, 24 Sep 2022 01:46:43 +0530 Subject: [PATCH 071/107] [FIX] `MongoInvalidArgumentError` on overwriting existing setting (#26880) Co-authored-by: Diego Sampaio <chinello@gmail.com> --- .../app/settings/server/SettingsRegistry.ts | 42 +++++++++---- .../server/functions/settings.mocks.ts | 18 +++++- .../server/functions/settings.tests.ts | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index 38daa398c31..a5966786068 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -1,6 +1,6 @@ import { Emitter } from '@rocket.chat/emitter'; import { isEqual } from 'underscore'; -import type { ISetting, ISettingGroup, SettingValue } from '@rocket.chat/core-typings'; +import type { ISetting, ISettingGroup, Optional, SettingValue } from '@rocket.chat/core-typings'; import { isSettingEnterprise } from '@rocket.chat/core-typings'; import type { ISettingsModel } from '@rocket.chat/model-typings'; @@ -157,23 +157,24 @@ export class SettingsRegistry { const overwrittenKeys = Object.keys(settingFromCodeOverwritten); const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); - await this.model.updateOne( - { _id }, - { - $set: { ...settingOverwrittenProps }, - ...(removedKeys.length && { - $unset: removedKeys.reduce((unset, key) => ({ ...unset, [key]: 1 }), {}), - }), - }, - { upsert: true }, - ); + const updatedProps = (() => { + return { + ...settingOverwrittenProps, + ...(settingStoredOverwritten && + settingStored.value !== settingStoredOverwritten.value && { value: settingStoredOverwritten.value }), + }; + })(); + await this.saveUpdatedSetting(_id, updatedProps, removedKeys); return; } if (settingStored && isOverwritten) { if (settingStored.value !== settingFromCodeOverwritten.value) { - await this.model.updateOne({ _id }, settingProps, { upsert: true }); + const overwrittenKeys = Object.keys(settingFromCodeOverwritten); + const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); + + await this.saveUpdatedSetting(_id, settingProps, removedKeys); } return; } @@ -263,4 +264,21 @@ export class SettingsRegistry { return groupSetWith({ group: _id })({}, callback); } + + private async saveUpdatedSetting( + _id: string, + settingProps: Omit<Optional<ISetting, 'value'>, '_id'>, + removedKeys?: string[], + ): Promise<void> { + await this.model.updateOne( + { _id }, + { + $set: settingProps, + ...(removedKeys?.length && { + $unset: removedKeys.reduce((unset, key) => ({ ...unset, [key]: 1 }), {}), + }), + }, + { upsert: true }, + ); + } } diff --git a/apps/meteor/app/settings/server/functions/settings.mocks.ts b/apps/meteor/app/settings/server/functions/settings.mocks.ts index 4612f8bc3ef..9cd409ba0b8 100644 --- a/apps/meteor/app/settings/server/functions/settings.mocks.ts +++ b/apps/meteor/app/settings/server/functions/settings.mocks.ts @@ -40,7 +40,7 @@ class SettingsClass { this.insertCalls++; } - updateOne(query: any, update: any): void { + updateOne(query: any, update: any, options?: any): void { const existent = this.findOne(query); const data = { ...existent, ...query, ...update, ...update.$set }; @@ -49,6 +49,22 @@ class SettingsClass { Object.assign(data, update.$setOnInsert); } + if (update.$unset) { + Object.keys(update.$unset).forEach((key) => { + delete data[key]; + }); + } + + const modifiers = ['$set', '$setOnInsert', '$unset']; + + modifiers.forEach((key) => { + delete data[key]; + }); + + if (options?.upsert === true && !modifiers.some((key) => Object.keys(update).includes(key))) { + throw new Error('Invalid upsert'); + } + // console.log(query, data); this.data.set(query._id, data); diff --git a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts index d2f2fd04630..96def7faba7 100644 --- a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts +++ b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts @@ -298,6 +298,66 @@ describe('Settings', () => { expect(Settings.findOne({ _id: 'my_setting' })).to.include({ ...expectedSetting }); }); + it('should respect override via environment when changing settings props', async () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + await settingsRegistry.addGroup('group', function () { + this.section('section', function () { + this.add('my_setting', 0, { + type: 'int', + sorter: 0, + }); + }); + }); + + const expectedSetting = { + value: 0, + type: 'int', + sorter: 0, + group: 'group', + section: 'section', + packageValue: 0, + hidden: false, + blocked: false, + secret: false, + i18nLabel: 'my_setting', + i18nDescription: 'my_setting_Description', + autocomplete: true, + }; + + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); + expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); + + process.env.OVERWRITE_SETTING_my_setting = '1'; + await settingsRegistry.addGroup('group', function () { + // removed section + this.add('my_setting', 0, { + type: 'int', + sorter: 0, + }); + }); + + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(1); + + const { section: _section, ...removedSection } = expectedSetting; + + const settingWithoutSection = { + ...removedSection, + value: 1, + processEnvValue: 1, + valueSource: 'processEnvValue', + }; + + expect(Settings.findOne({ _id: 'my_setting' })) + .to.include({ ...settingWithoutSection }) + .to.not.have.any.keys('section'); + }); + it('should call `settings.get` callback on setting added', async () => { return new Promise(async (resolve) => { const settings = new CachedSettings(); -- GitLab From 71a4cb7933a9695afd12120c440330a3ecb5ecf4 Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Fri, 23 Sep 2022 19:55:05 -0300 Subject: [PATCH 072/107] [NEW] Move administration links to an exclusive kebab menu (#26867) --- apps/meteor/app/livechat/client/ui.js | 2 +- .../app/ui-utils/client/lib/AccountBox.ts | 9 +- .../AdministrationList/AdministrationList.tsx | 56 ++++++ .../AdministrationModelList.tsx | 93 ++++++++++ .../AdministrationList/AppsModelList.tsx | 65 +++++++ .../AdministrationList/AuditModelList.tsx | 48 ++++++ .../AdministrationList/SettingsModelList.tsx | 32 ++++ .../client/components/Sidebar/ListItem.tsx | 4 +- .../client/sidebar/header/UserDropdown.tsx | 101 +---------- .../sidebar/header/actions/Administration.tsx | 78 +++++++++ apps/meteor/client/sidebar/header/index.tsx | 2 + .../client/views/admin/sidebar/UpgradeTab.tsx | 19 +-- .../admin/upgrade/UpgradePage/UpgradePage.tsx | 3 +- .../client/views/hooks/useUpgradeTabParams.ts | 4 +- apps/meteor/ee/app/auditing/client/index.js | 16 -- apps/meteor/lib/getUpgradeTabType.ts | 43 ----- apps/meteor/lib/upgradeTab.ts | 46 +++++ .../rocketchat-i18n/i18n/en.i18n.json | 4 + .../tests/e2e/administration-menu.spec.ts | 60 +++++++ .../page-objects/fragments/home-sidenav.ts | 5 + .../AdministrationList.spec.tsx | 97 +++++++++++ .../AdministrationModelList.spec.tsx | 161 ++++++++++++++++++ .../AdministrationList/AppsModelList.spec.tsx | 97 +++++++++++ .../AuditModelList.spec.tsx | 57 +++++++ .../SettingsModelList.spec.tsx | 32 ++++ .../header/actions/Administration.spec.tsx | 114 +++++++++++++ ...radeTabType.spec.ts => upgradeTab.spec.ts} | 46 +++-- 27 files changed, 1092 insertions(+), 202 deletions(-) create mode 100644 apps/meteor/client/components/AdministrationList/AdministrationList.tsx create mode 100644 apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx create mode 100644 apps/meteor/client/components/AdministrationList/AppsModelList.tsx create mode 100644 apps/meteor/client/components/AdministrationList/AuditModelList.tsx create mode 100644 apps/meteor/client/components/AdministrationList/SettingsModelList.tsx create mode 100644 apps/meteor/client/sidebar/header/actions/Administration.tsx delete mode 100644 apps/meteor/lib/getUpgradeTabType.ts create mode 100644 apps/meteor/lib/upgradeTab.ts create mode 100644 apps/meteor/tests/e2e/administration-menu.spec.ts create mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx create mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx create mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx create mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/AuditModelList.spec.tsx create mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx create mode 100644 apps/meteor/tests/unit/client/siderbar/header/actions/Administration.spec.tsx rename apps/meteor/tests/unit/lib/{getUpgradeTabType.spec.ts => upgradeTab.spec.ts} (53%) diff --git a/apps/meteor/app/livechat/client/ui.js b/apps/meteor/app/livechat/client/ui.js index 6b7eade52a1..742ffd8ad7c 100644 --- a/apps/meteor/app/livechat/client/ui.js +++ b/apps/meteor/app/livechat/client/ui.js @@ -14,7 +14,7 @@ Tracker.autorun((c) => { }); AccountBox.addItem({ - name: 'Omnichannel', + name: 'Manage_Omnichannel', icon: 'headset', href: '/omnichannel/current', sideNav: 'omnichannelFlex', diff --git a/apps/meteor/app/ui-utils/client/lib/AccountBox.ts b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts index 1c95cc8a0f2..8e7d7d8137f 100644 --- a/apps/meteor/app/ui-utils/client/lib/AccountBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts @@ -2,6 +2,9 @@ import type { IUIActionButton, IUActionButtonWhen } from '@rocket.chat/apps-engi import type { UserStatus } from '@rocket.chat/core-typings'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { Icon } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; import { SideNav } from './SideNav'; import { applyDropdownActionButtonFilters } from '../../../ui-message/client/actionButtons/lib/applyButtonFilters'; @@ -17,9 +20,9 @@ export interface IAppAccountBoxItem extends IUIActionButton { when?: Omit<IUActionButtonWhen, 'roomTypes' | 'messageActionContext'>; } -type AccountBoxItem = { - name: string; - icon: string; +export type AccountBoxItem = { + name: TranslationKey; + icon: ComponentProps<typeof Icon>['name']; href: string; sideNav?: string; condition: () => boolean; diff --git a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx new file mode 100644 index 00000000000..e9a570ffd21 --- /dev/null +++ b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx @@ -0,0 +1,56 @@ +import { OptionDivider } from '@rocket.chat/fuselage'; +import React, { FC, Fragment } from 'react'; + +import { AccountBoxItem, IAppAccountBoxItem, isAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; +import AdministrationModelList from './AdministrationModelList'; +import AppsModelList from './AppsModelList'; +import AuditModelList from './AuditModelList'; +import SettingsModelList from './SettingsModelList'; + +type AdministrationListProps = { + accountBoxItems: (IAppAccountBoxItem | AccountBoxItem)[]; + closeList: () => void; + hasAdminPermission: boolean; + hasAuditLicense: boolean; + hasAuditPermission: boolean; + hasAuditLogPermission: boolean; + hasManageApps: boolean; + hasSettingsPermission: boolean; +}; + +const AdministrationList: FC<AdministrationListProps> = ({ + accountBoxItems, + hasAuditPermission, + hasAuditLogPermission, + hasManageApps, + hasSettingsPermission, + hasAdminPermission, + closeList, +}) => { + const appBoxItems = accountBoxItems.filter((item): item is IAppAccountBoxItem => isAppAccountBoxItem(item)); + const adminBoxItems = accountBoxItems.filter((item): item is AccountBoxItem => !isAppAccountBoxItem(item)); + const showAudit = hasAuditPermission || hasAuditLogPermission; + const showManageApps = hasManageApps || !!appBoxItems.length; + const showAdmin = hasAdminPermission || !!adminBoxItems.length; + const showSettings = hasSettingsPermission; + + const list = [ + showAdmin && <AdministrationModelList showAdmin={showAdmin} accountBoxItems={adminBoxItems} closeList={closeList} />, + showSettings && <SettingsModelList closeList={closeList} />, + showManageApps && <AppsModelList appBoxItems={appBoxItems} closeList={closeList} showManageApps={showManageApps} />, + showAudit && <AuditModelList showAudit={hasAuditPermission} showAuditLog={hasAuditLogPermission} closeList={closeList} />, + ]; + + return ( + <> + {list.filter(Boolean).map((item, index) => ( + <Fragment key={index}> + {index > 0 && <OptionDivider />} + {item} + </Fragment> + ))} + </> + ); +}; + +export default AdministrationList; diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx new file mode 100644 index 00000000000..312972d5aee --- /dev/null +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx @@ -0,0 +1,93 @@ +import { OptionTitle } from '@rocket.chat/fuselage'; +import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import React, { FC } from 'react'; + +import { userHasAllPermission } from '../../../app/authorization/client'; +import { SideNav } from '../../../app/ui-utils/client'; +import { AccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; +import { getUpgradeTabLabel, isFullyFeature } from '../../../lib/upgradeTab'; +import { useUpgradeTabParams } from '../../views/hooks/useUpgradeTabParams'; +import Emoji from '../Emoji'; +import ListItem from '../Sidebar/ListItem'; + +type AdministrationModelListProps = { + accountBoxItems: AccountBoxItem[]; + showAdmin: boolean; + closeList: () => void; +}; + +const INFO_PERMISSIONS = ['view-statistics']; + +const AdministrationModelList: FC<AdministrationModelListProps> = ({ accountBoxItems, showAdmin, closeList }) => { + const t = useTranslation(); + const { tabType, trialEndDate, isLoading } = useUpgradeTabParams(); + const shouldShowEmoji = isFullyFeature(tabType); + const label = getUpgradeTabLabel(tabType); + const hasInfoPermission = userHasAllPermission(INFO_PERMISSIONS); + + const infoRoute = useRoute('admin-info'); + const adminRoute = useRoute('admin-index'); + const upgradeRoute = useRoute('upgrade'); + const showUpgradeItem = !isLoading && tabType; + + return ( + <> + <OptionTitle>{t('Administration')}</OptionTitle> + <ul> + {showAdmin && ( + <> + {showUpgradeItem && ( + <ListItem + icon='arrow-stack-up' + text={ + <> + {t(label)} {shouldShowEmoji && <Emoji emojiHandle=':zap:' />} + </> + } + action={(): void => { + upgradeRoute.push({ type: tabType }, trialEndDate ? { trialEndDate } : undefined); + closeList(); + }} + /> + )} + <ListItem + icon='cog' + text={t('Manage_workspace')} + action={(): void => { + if (hasInfoPermission) { + infoRoute.push(); + closeList(); + return; + } + + adminRoute.push({ context: '/' }); + closeList(); + }} + /> + </> + )} + {accountBoxItems.length > 0 && ( + <> + {accountBoxItems.map((item, key) => { + const action = (): void => { + if (item.href) { + FlowRouter.go(item.href); + } + if (item.sideNav) { + SideNav.setFlex(item.sideNav); + SideNav.openFlex(); + } + closeList(); + }; + + return <ListItem text={t(item.name)} icon={item.icon} action={action} key={item.name + key} />; + })} + </> + )} + </ul> + </> + ); +}; + +export default AdministrationModelList; diff --git a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx new file mode 100644 index 00000000000..6050ff5d1e3 --- /dev/null +++ b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx @@ -0,0 +1,65 @@ +import { OptionTitle } from '@rocket.chat/fuselage'; +import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; +import React, { FC } from 'react'; + +import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager'; +import { IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; +import ListItem from '../Sidebar/ListItem'; + +type AppsModelListProps = { + appBoxItems: IAppAccountBoxItem[]; + showManageApps: boolean; + closeList: () => void; +}; + +const AppsModelList: FC<AppsModelListProps> = ({ appBoxItems, showManageApps, closeList }) => { + const t = useTranslation(); + const marketplaceRoute = useRoute('admin-marketplace'); + + return ( + <> + <OptionTitle>{t('Apps')}</OptionTitle> + <ul> + {showManageApps && ( + <> + <ListItem + icon='cube' + text={t('Marketplace')} + action={(): void => { + marketplaceRoute.push(); + closeList(); + }} + /> + <ListItem + icon='cube' + text={t('Installed')} + action={(): void => { + marketplaceRoute.push({ context: 'installed' }); + closeList(); + }} + /> + </> + )} + {appBoxItems.length > 0 && ( + <> + {appBoxItems.map((item, key) => { + const action = (): void => { + triggerActionButtonAction({ + rid: '', + mid: '', + actionId: item.actionId, + appId: item.appId, + payload: { context: item.context }, + }); + closeList(); + }; + return <ListItem text={(t.has(item.name) && t(item.name)) || item.name} action={action} key={item.actionId + key} />; + })} + </> + )} + </ul> + </> + ); +}; + +export default AppsModelList; diff --git a/apps/meteor/client/components/AdministrationList/AuditModelList.tsx b/apps/meteor/client/components/AdministrationList/AuditModelList.tsx new file mode 100644 index 00000000000..4e476e7745f --- /dev/null +++ b/apps/meteor/client/components/AdministrationList/AuditModelList.tsx @@ -0,0 +1,48 @@ +import { OptionTitle } from '@rocket.chat/fuselage'; +import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { FC } from 'react'; + +import ListItem from '../Sidebar/ListItem'; + +type AuditModelListProps = { + closeList: () => void; + showAudit: boolean; + showAuditLog: boolean; +}; + +const AuditModelList: FC<AuditModelListProps> = ({ showAudit, showAuditLog, closeList }) => { + const t = useTranslation(); + + const auditHomeRoute = useRoute('audit-home'); + const auditSettingsRoute = useRoute('audit-log'); + + return ( + <> + <OptionTitle>{t('Audit')}</OptionTitle> + <ul> + {showAudit && ( + <ListItem + icon='document-eye' + text={t('Messages')} + action={(): void => { + auditHomeRoute.push(); + closeList(); + }} + /> + )} + {showAuditLog && ( + <ListItem + icon='document-eye' + text={t('Logs')} + action={(): void => { + auditSettingsRoute.push(); + closeList(); + }} + /> + )} + </ul> + </> + ); +}; + +export default AuditModelList; diff --git a/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx b/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx new file mode 100644 index 00000000000..245640aa119 --- /dev/null +++ b/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx @@ -0,0 +1,32 @@ +import { OptionTitle } from '@rocket.chat/fuselage'; +import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; +import React, { FC } from 'react'; + +import ListItem from '../Sidebar/ListItem'; + +type SettingsModelListProps = { + closeList: () => void; +}; + +const SettingsModelList: FC<SettingsModelListProps> = ({ closeList }) => { + const t = useTranslation(); + const settingsRoute = useRoute('admin-settings'); + + return ( + <> + <OptionTitle>{t('Settings')}</OptionTitle> + <ul> + <ListItem + icon='customize' + text={t('Workspace_settings')} + action={(): void => { + settingsRoute.push(); + closeList(); + }} + /> + </ul> + </> + ); +}; + +export default SettingsModelList; diff --git a/apps/meteor/client/components/Sidebar/ListItem.tsx b/apps/meteor/client/components/Sidebar/ListItem.tsx index 28763e5f047..e4a899c83c9 100644 --- a/apps/meteor/client/components/Sidebar/ListItem.tsx +++ b/apps/meteor/client/components/Sidebar/ListItem.tsx @@ -1,8 +1,8 @@ import { Option, OptionColumn, OptionContent, OptionIcon } from '@rocket.chat/fuselage'; -import React, { ComponentProps, MouseEventHandler, ReactElement } from 'react'; +import React, { ComponentProps, MouseEventHandler, ReactElement, ReactNode } from 'react'; type ListItemProps = { - text: string; + text: ReactNode; icon?: ComponentProps<typeof OptionIcon>['name']; input?: any; action?: MouseEventHandler<HTMLOrSVGElement>; diff --git a/apps/meteor/client/sidebar/header/UserDropdown.tsx b/apps/meteor/client/sidebar/header/UserDropdown.tsx index c96bf515bdc..86a97b19fa1 100644 --- a/apps/meteor/client/sidebar/header/UserDropdown.tsx +++ b/apps/meteor/client/sidebar/header/UserDropdown.tsx @@ -2,41 +2,19 @@ import type { IUser } from '@rocket.chat/core-typings'; import { UserStatus as UserStatusEnum, ValueOf } from '@rocket.chat/core-typings'; import { Box, Margins, Option, OptionColumn, OptionContent, OptionDivider, OptionTitle } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useLayout, useRoute, useLogout, useSetting, useAtLeastOnePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import { FlowRouter } from 'meteor/kadira:flow-router'; +import { useLayout, useRoute, useLogout, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; -import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager'; -import { AccountBox, SideNav } from '../../../app/ui-utils/client'; -import { IAppAccountBoxItem, isAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; +import { AccountBox } from '../../../app/ui-utils/client'; import { userStatus } from '../../../app/user-status/client'; import { callbacks } from '../../../lib/callbacks'; import MarkdownText from '../../components/MarkdownText'; import { UserStatus } from '../../components/UserStatus'; import UserAvatar from '../../components/avatar/UserAvatar'; -import { useReactiveValue } from '../../hooks/useReactiveValue'; import { useUserDisplayName } from '../../hooks/useUserDisplayName'; import { imperativeModal } from '../../lib/imperativeModal'; import EditStatusModal from './EditStatusModal'; -const ADMIN_PERMISSIONS = [ - 'view-logs', - 'manage-emoji', - 'manage-sounds', - 'view-statistics', - 'manage-oauth-apps', - 'view-privileged-setting', - 'manage-selected-settings', - 'view-room-administration', - 'view-user-administration', - 'access-setting-permissions', - 'manage-outgoing-integrations', - 'manage-incoming-integrations', - 'manage-own-outgoing-integrations', - 'manage-own-incoming-integrations', - 'view-engagement-dashboard', -]; - const isDefaultStatus = (id: string): boolean => (Object.values(UserStatusEnum) as string[]).includes(id); const isDefaultStatusName = (_name: string, id: string): _name is UserStatusEnum => isDefaultStatus(id); @@ -46,8 +24,6 @@ const setStatus = (status: typeof userStatus.list['']): void => { callbacks.run('userStatusManuallySet', status); }; -const getItems = (): ReturnType<typeof AccountBox.getItems> => AccountBox.getItems(); - const translateStatusName = (t: ReturnType<typeof useTranslation>, status: typeof userStatus.list['']): string => { if (isDefaultStatusName(status.name, status.id)) { return t(status.name); @@ -64,9 +40,8 @@ type UserDropdownProps = { const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { const t = useTranslation(); const accountRoute = useRoute('account-index'); - const adminRoute = useRoute('admin-index'); const logout = useLogout(); - const { sidebar, isMobile } = useLayout(); + const { isMobile } = useLayout(); const { username, avatarETag, status, statusText } = user; @@ -76,8 +51,6 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { ? (status: ValueOf<typeof userStatus['list']>): boolean => status.name !== 'invisible' : (): boolean => true; - const showAdmin = useAtLeastOnePermission(ADMIN_PERMISSIONS); - const handleCustomStatus = useMutableCallback((e) => { e.preventDefault(); imperativeModal.open({ @@ -92,21 +65,11 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { onClose(); }); - const handleAdmin = useMutableCallback(() => { - adminRoute.push(); - sidebar.toggle(); - onClose(); - }); - const handleLogout = useMutableCallback(() => { logout(); onClose(); }); - const accountBoxItems = useReactiveValue(getItems); - - const appBoxItems = (): IAppAccountBoxItem[] => accountBoxItems.filter((item): item is IAppAccountBoxItem => isAppAccountBoxItem(item)); - return ( <Box display='flex' flexDirection='column' w={!isMobile ? '244px' : undefined}> <Box pi='x12' display='flex' flexDirection='row' alignItems='center'> @@ -158,64 +121,6 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { ); })} <Option icon='emoji' label={`${t('Custom_Status')}...`} onClick={handleCustomStatus}></Option> - - {(accountBoxItems.length || showAdmin) && ( - <> - <OptionDivider /> - {showAdmin && <Option icon={'customize'} label={t('Administration')} onClick={handleAdmin}></Option>} - {accountBoxItems - .filter((item) => !isAppAccountBoxItem(item)) - .map((item, i) => { - const action = (): void => { - if (item.href) { - FlowRouter.go(item.href); - onClose(); - } - if (item.sideNav) { - SideNav.setFlex(item.sideNav); - SideNav.openFlex(); - onClose(); - } - }; - - return ( - <Option - icon={item.icon as any} - label={t(item.name as any)} - onClick={item.href || item.sideNav ? action : undefined} - key={i} - ></Option> - ); - })} - </> - )} - - {appBoxItems().length > 0 && ( - <> - <OptionDivider /> - <Box pi='x16' fontScale='c1' textTransform='uppercase'> - {t('Apps')} - </Box> - {appBoxItems().map((item, key) => { - const action = (): void => { - triggerActionButtonAction({ - rid: '', - mid: '', - actionId: item.actionId, - appId: item.appId, - payload: { context: item.context }, - }); - }; - return ( - // We use the type assertion to any in the `label` property as i18n strings that come from apps are not known in compile time - <> - <Option label={t(item.name as any)} key={item.actionId + key} onClick={action} /> - </> - ); - })} - </> - )} - <OptionDivider /> <Option icon='user' label={t('My_Account')} onClick={handleMyAccount}></Option> <Option icon='sign-out' label={t('Logout')} onClick={handleLogout}></Option> diff --git a/apps/meteor/client/sidebar/header/actions/Administration.tsx b/apps/meteor/client/sidebar/header/actions/Administration.tsx new file mode 100644 index 00000000000..3669dde3ac3 --- /dev/null +++ b/apps/meteor/client/sidebar/header/actions/Administration.tsx @@ -0,0 +1,78 @@ +import { Sidebar, Dropdown } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; +import React, { HTMLAttributes, useRef, VFC } from 'react'; +import { createPortal } from 'react-dom'; + +import { AccountBox } from '../../../../app/ui-utils/client'; +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import AdministrationList from '../../../components/AdministrationList/AdministrationList'; +import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { useDropdownVisibility } from '../hooks/useDropdownVisibility'; + +const ADMIN_PERMISSIONS = [ + 'view-logs', + 'manage-emoji', + 'manage-sounds', + 'view-statistics', + 'manage-oauth-apps', + 'view-privileged-setting', + 'manage-selected-settings', + 'view-room-administration', + 'view-user-administration', + 'access-setting-permissions', + 'manage-outgoing-integrations', + 'manage-incoming-integrations', + 'manage-own-outgoing-integrations', + 'manage-own-incoming-integrations', + 'view-engagement-dashboard', +]; +const SETTINGS_PERMISSIONS = ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']; +const AUDIT_PERMISSIONS = ['can-audit']; +const AUDIT_LOG_PERMISSIONS = ['can-audit-log']; +const MANAGE_APPS_PERMISSIONS = ['manage-apps']; + +const AUDIT_LICENSE_MODULE = 'auditing'; + +const Administration: VFC<Omit<HTMLAttributes<HTMLElement>, 'is'>> = (props) => { + const reference = useRef(null); + const target = useRef(null); + + const { isVisible, toggle } = useDropdownVisibility({ reference, target }); + + const getAccountBoxItems = useMutableCallback(() => AccountBox.getItems()); + const accountBoxItems = useReactiveValue(getAccountBoxItems); + + const hasAuditLicense = useHasLicenseModule(AUDIT_LICENSE_MODULE) === true; + const hasAuditPermission = useAtLeastOnePermission(AUDIT_PERMISSIONS) && hasAuditLicense; + const hasAuditLogPermission = useAtLeastOnePermission(AUDIT_LOG_PERMISSIONS) && hasAuditLicense; + const hasManageApps = useAtLeastOnePermission(MANAGE_APPS_PERMISSIONS); + const hasAdminPermission = useAtLeastOnePermission(ADMIN_PERMISSIONS); + const hasSettingsPermission = useAtLeastOnePermission(SETTINGS_PERMISSIONS); + const showMenu = + hasAuditPermission || hasAuditLogPermission || hasManageApps || hasAdminPermission || hasSettingsPermission || !!accountBoxItems.length; + + return ( + <> + {showMenu && <Sidebar.TopBar.Action icon='menu' onClick={(): void => toggle()} {...props} ref={reference} />} + {isVisible && + createPortal( + <Dropdown reference={reference} ref={target}> + <AdministrationList + accountBoxItems={accountBoxItems} + closeList={(): void => toggle(false)} + hasAdminPermission={hasAdminPermission} + hasAuditLicense={hasAuditLicense} + hasAuditPermission={hasAuditPermission} + hasAuditLogPermission={hasAuditLogPermission} + hasManageApps={hasManageApps} + hasSettingsPermission={hasSettingsPermission} + /> + </Dropdown>, + document.body, + )} + </> + ); +}; + +export default Administration; diff --git a/apps/meteor/client/sidebar/header/index.tsx b/apps/meteor/client/sidebar/header/index.tsx index a65a5b305c3..36c8113b450 100644 --- a/apps/meteor/client/sidebar/header/index.tsx +++ b/apps/meteor/client/sidebar/header/index.tsx @@ -4,6 +4,7 @@ import React, { memo, ReactElement } from 'react'; import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor'; import UserAvatarButton from './UserAvatarButton'; +import Administration from './actions/Administration'; import CreateRoom from './actions/CreateRoom'; import Directory from './actions/Directory'; import Home from './actions/Home'; @@ -28,6 +29,7 @@ const HeaderWithData = (): ReactElement => { <Directory title={t('Directory')} /> <Sort title={t('Display')} /> <CreateRoom title={t('Create_new')} data-qa='sidebar-create' /> + <Administration title={t('Administration')} /> </> )} {!user && <Login title={t('Login')} />} diff --git a/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx b/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx index 65a144f027a..b64c5ba0e8d 100644 --- a/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx +++ b/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx @@ -1,25 +1,12 @@ import { Box, Icon } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { useRoutePath, useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; +import { useRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useMemo } from 'react'; -import type { UpgradeTabVariant } from '../../../../lib/getUpgradeTabType'; +import { getUpgradeTabLabel, isFullyFeature, UpgradeTabVariant } from '../../../../lib/upgradeTab'; import Emoji from '../../../components/Emoji'; import Sidebar from '../../../components/Sidebar'; -const getUpgradeTabLabel = (type: UpgradeTabVariant): TranslationKey => { - switch (type) { - case 'go-fully-featured': - case 'go-fully-featured-registered': - return 'Upgrade_tab_go_fully_featured'; - case 'trial-gold': - case 'trial-enterprise': - return 'Upgrade_tab_trial_guide'; - case 'upgrade-your-plan': - return 'Upgrade_tab_upgrade_your_plan'; - } -}; - const customColors = { default: colors['s2-700'], hover: colors['s2-800'], @@ -42,7 +29,7 @@ const UpgradeTab = ({ type, currentPath, trialEndDate }: UpgradeTabProps): React const t = useTranslation(); const label = getUpgradeTabLabel(type); - const displayEmoji = type === 'go-fully-featured'; + const displayEmoji = isFullyFeature(type); return ( <Sidebar.GenericItem active={currentPath === path} href={String(path)} customColors={customColors} textColor='alternative'> diff --git a/apps/meteor/client/views/admin/upgrade/UpgradePage/UpgradePage.tsx b/apps/meteor/client/views/admin/upgrade/UpgradePage/UpgradePage.tsx index 62b71ef6a6c..7950dce0890 100644 --- a/apps/meteor/client/views/admin/upgrade/UpgradePage/UpgradePage.tsx +++ b/apps/meteor/client/views/admin/upgrade/UpgradePage/UpgradePage.tsx @@ -2,7 +2,7 @@ import { Throbber, Box } from '@rocket.chat/fuselage'; import { useLayout, useRouteParameter, useQueryStringParameter, useAbsoluteUrl, useLanguage } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useEffect, useRef, useState } from 'react'; -import type { UpgradeTabVariant } from '../../../../../lib/getUpgradeTabType'; +import type { UpgradeTabVariant } from '../../../../../lib/upgradeTab'; import Page from '../../../../components/Page'; import PageHeader from '../../../../components/Page/PageHeader'; import UpgradePageError from '../UpgradePageError'; @@ -10,7 +10,6 @@ import UpgradePageError from '../UpgradePageError'; const urlMap: Record<UpgradeTabVariant, string> = { 'go-fully-featured': 'https://go.rocket.chat/i/upgrade-ce-1-unregistered', 'go-fully-featured-registered': 'https://go.rocket.chat/i/upgrade-ce-1-registered', - 'trial-gold': 'https://go.rocket.chat/i/upgrade-gold-trial', 'trial-enterprise': 'https://go.rocket.chat/i/upgrade-ee-trial', 'upgrade-your-plan': 'https://go.rocket.chat/i/upgrade-ce-2', }; diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index f1a5955f0ca..5a9d946f584 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -2,7 +2,7 @@ import { useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import { format } from 'date-fns'; -import { UpgradeTabVariant, getUpgradeTabType } from '../../../lib/getUpgradeTabType'; +import { getUpgradeTabType, UpgradeTabVariant } from '../../../lib/upgradeTab'; export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; trialEndDate: string | undefined; isLoading: boolean } => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); @@ -23,7 +23,6 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const trialLicense = licenses?.find(({ meta }) => meta?.trial); const isTrial = licenses?.every(({ meta }) => meta?.trial) ?? false; - const hasGoldLicense = licenses?.some(({ tag }) => tag?.name === 'Gold') ?? false; const trialEndDate = trialLicense?.meta ? format(new Date(trialLicense.meta.trialEnd), 'yyyy-MM-dd') : undefined; const upgradeTabType = getUpgradeTabType({ @@ -31,7 +30,6 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri hasValidLicense, hadExpiredTrials, isTrial, - hasGoldLicense, }); return { tabType: upgradeTabType, trialEndDate, isLoading: !isSuccess }; diff --git a/apps/meteor/ee/app/auditing/client/index.js b/apps/meteor/ee/app/auditing/client/index.js index 010e3a0c42e..bab254951d8 100644 --- a/apps/meteor/ee/app/auditing/client/index.js +++ b/apps/meteor/ee/app/auditing/client/index.js @@ -1,6 +1,4 @@ import { hasLicense } from '../../license/client'; -import { AccountBox } from '../../../../app/ui-utils/client'; -import { hasAllPermission } from '../../../../app/authorization/client'; hasLicense('auditing') .then((enabled) => { @@ -9,20 +7,6 @@ hasLicense('auditing') } require('./templates'); require('./index.css'); - - AccountBox.addItem({ - href: 'audit-home', - name: 'Message_auditing', - icon: 'document-eye', - condition: () => hasAllPermission('can-audit'), - }); - - AccountBox.addItem({ - href: 'audit-log', - name: 'Message_auditing_log', - icon: 'document-eye', - condition: () => hasAllPermission('can-audit-log'), - }); }) .catch((error) => { console.error('Error checking license.', error); diff --git a/apps/meteor/lib/getUpgradeTabType.ts b/apps/meteor/lib/getUpgradeTabType.ts deleted file mode 100644 index 7bfeb01b4dc..00000000000 --- a/apps/meteor/lib/getUpgradeTabType.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type UpgradeTabVariant = - | 'go-fully-featured' - | 'go-fully-featured-registered' - | 'trial-gold' - | 'trial-enterprise' - | 'upgrade-your-plan'; - -type UpgradeTabConditions = { - registered: boolean; - hasValidLicense: boolean; - isTrial: boolean; - hadExpiredTrials: boolean; - hasGoldLicense: boolean; -}; - -export const getUpgradeTabType = ({ - registered, - hasValidLicense, - isTrial, - hasGoldLicense, - hadExpiredTrials, -}: UpgradeTabConditions): UpgradeTabVariant | false => { - if (!hasValidLicense) { - if (hadExpiredTrials) { - return 'upgrade-your-plan'; - } - - if (registered) { - return 'go-fully-featured-registered'; - } - - return 'go-fully-featured'; - } - - if (isTrial) { - if (hasGoldLicense) { - return 'trial-gold'; - } - return 'trial-enterprise'; - } - - return false; -}; diff --git a/apps/meteor/lib/upgradeTab.ts b/apps/meteor/lib/upgradeTab.ts new file mode 100644 index 00000000000..6c5bdfdb93f --- /dev/null +++ b/apps/meteor/lib/upgradeTab.ts @@ -0,0 +1,46 @@ +export type UpgradeTabVariant = 'go-fully-featured' | 'go-fully-featured-registered' | 'trial-enterprise' | 'upgrade-your-plan'; + +type UpgradeLabel = 'Upgrade_tab_upgrade_your_plan' | 'Upgrade_tab_trial_guide' | 'Upgrade_tab_go_fully_featured'; + +type UpgradeTabConditions = { + registered: boolean; + hasValidLicense: boolean; + isTrial: boolean; + hadExpiredTrials: boolean; +}; + +export const getUpgradeTabType = ({ + registered, + hasValidLicense, + isTrial, + hadExpiredTrials, +}: UpgradeTabConditions): UpgradeTabVariant | false => { + if (!hasValidLicense) { + if (hadExpiredTrials) { + return 'upgrade-your-plan'; + } + if (registered) { + return 'go-fully-featured-registered'; + } + return 'go-fully-featured'; + } + if (isTrial) { + return 'trial-enterprise'; + } + + return false; +}; + +export const getUpgradeTabLabel = (type: UpgradeTabVariant | false): UpgradeLabel => { + switch (type) { + case 'go-fully-featured': + case 'go-fully-featured-registered': + return 'Upgrade_tab_go_fully_featured'; + case 'trial-enterprise': + return 'Upgrade_tab_trial_guide'; + default: + return 'Upgrade_tab_upgrade_your_plan'; + } +}; + +export const isFullyFeature = (type: UpgradeTabVariant | false) => type === 'go-fully-featured'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index ac81f641efa..fc4904d5f98 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -615,6 +615,7 @@ "Audio_Notifications_Default_Alert": "Audio Notifications Default Alert", "Audio_Notifications_Value": "Default Message Notification Audio", "Audios": "Audios", + "Audit": "Audit", "Auditing": "Auditing", "Auth": "Auth", "Auth_Token": "Auth Token", @@ -3082,6 +3083,8 @@ "manage-voip-call-settings_description": "Permission to manage voip call settings", "manage-voip-contact-center-settings": "Manage Voip Contact Center Settings", "manage-voip-contact-center-settings_description": "Permission to manage voip contact center settings", + "Manage_Omnichannel": "Manage Omnichannel", + "Manage_workspace": "Manage workspace", "Manager_added": "Manager added", "Manager_removed": "Manager removed", "Managers": "Managers", @@ -5302,6 +5305,7 @@ "Would_you_like_to_place_chat_on_hold": "Would you like to place this chat On-Hold?", "Wrap_up_the_call": "Wrap-up the call", "Wrap_Up_Notes": "Wrap-Up Notes", + "Workspace_settings": "Workspace settings", "Yes": "Yes", "Yes_archive_it": "Yes, archive it!", "Yes_clear_all": "Yes, clear all!", diff --git a/apps/meteor/tests/e2e/administration-menu.spec.ts b/apps/meteor/tests/e2e/administration-menu.spec.ts new file mode 100644 index 00000000000..701965e12f7 --- /dev/null +++ b/apps/meteor/tests/e2e/administration-menu.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from './utils/test'; +import { HomeDiscussion } from './page-objects'; +import { IS_EE } from './config/constants'; + +test.use({ storageState: 'admin-session.json' }); + +test.describe.serial('administration-menu', () => { + let poHomeDiscussion: HomeDiscussion; + + test.beforeEach(async ({ page }) => { + poHomeDiscussion = new HomeDiscussion(page); + + await page.goto('/home'); + }); + + test('expect open upgrade page', async ({ page }) => { + test.skip(IS_EE, 'Community Only'); + await poHomeDiscussion.sidenav.openAdministrationByLabel('Go fully featured'); + + await expect(page).toHaveURL('admin/upgrade/go-fully-featured'); + }); + + test('expect open info page', async ({ page }) => { + await poHomeDiscussion.sidenav.openAdministrationByLabel('Manage workspace'); + + await expect(page).toHaveURL('admin/info'); + }); + + test('expect open omnichannel page', async ({ page }) => { + await poHomeDiscussion.sidenav.openAdministrationByLabel('Manage Omnichannel'); + + await expect(page).toHaveURL('omnichannel/current'); + }); + + test('expect open settings page', async ({ page }) => { + await poHomeDiscussion.sidenav.openAdministrationByLabel('Workspace settings'); + + await expect(page).toHaveURL('admin/settings'); + }); + + test('expect open app marketplace page', async ({ page }) => { + await poHomeDiscussion.sidenav.openAdministrationByLabel('Marketplace'); + + await expect(page).toHaveURL('admin/marketplace'); + }); + + test('expect open app installed page', async ({ page }) => { + await poHomeDiscussion.sidenav.openAdministrationByLabel('Installed'); + + await expect(page).toHaveURL('admin/marketplace/installed'); + }); + + test.describe('user', () => { + test.use({ storageState: 'user1-session.json' }); + + test('expect to not render administration menu when no permission', async ({ page }) => { + await expect(page.locator('role=button[name="Administration"]')).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 56361b92ec3..0722895e622 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -21,6 +21,11 @@ export class HomeSidenav { return this.page.locator('//*[@id="modal-root"]//button[contains(text(), "Create")]'); } + async openAdministrationByLabel(text: string): Promise<void> { + await this.page.locator('role=button[name="Administration"]').click(); + await this.page.locator(`li.rcx-option >> text="${text}"`).click(); + } + async openNewByLabel(text: string): Promise<void> { await this.page.locator('[data-qa="sidebar-create"]').click(); await this.page.locator(`li.rcx-option >> text="${text}"`).click(); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx new file mode 100644 index 00000000000..bde3be57328 --- /dev/null +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx @@ -0,0 +1,97 @@ +import { render, screen } from '@testing-library/react'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import React from 'react'; + +const COMPONENT_PATH = '../../../../../client/components/AdministrationList/AdministrationList'; +const defaultConfig = { + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => true, + }, + '../../../app/ui-utils/client': { + SideNav: {}, + }, + '../../../app/ui-utils/client/lib/AccountBox': { + AccountBoxItem: {}, + isAppAccountBoxItem: () => false, + }, + '../../../ee/client/hooks/useHasLicenseModule': { + useHasLicenseModule: () => true, + }, + './AdministrationModelList': () => <p>Administration Model List</p>, + './AppsModelList': () => <p>Apps Model List</p>, + './AuditModelList': () => <p>Audit Model List</p>, + './SettingsModelList': () => <p>Settings Model List</p>, +}; + +describe('components/AdministrationList/AdministrationList', () => { + it('should render all model list', async () => { + const AdministrationList = proxyquire.noCallThru().load(COMPONENT_PATH, defaultConfig).default; + render( + <AdministrationList + closeList={() => null} + accountBoxItems={[{}]} + hasAuditPermission={true} + hasAuditLogPermission={true} + hasManageApps={true} + hasSettingsPermission={true} + hasAdminPermission={true} + />, + ); + + expect(screen.getByText('Administration Model List')).to.exist; + expect(screen.getByText('Apps Model List')).to.exist; + expect(screen.getByText('Settings Model List')).to.exist; + expect(screen.getByText('Audit Model List')).to.exist; + }); + + it('should render nothing when no permission', async () => { + const AdministrationList = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => false, + }, + }).default; + render(<AdministrationList closeList={() => null} accountBoxItems={[]} />); + + expect(screen.queryByText('Administration Model List')).to.not.exist; + expect(screen.queryByText('Apps Model List')).to.not.exist; + expect(screen.queryByText('Settings Model List')).to.not.exist; + expect(screen.queryByText('Audit Model List')).to.not.exist; + }); + + it('should render administration model list when has account box item', async () => { + const AdministrationList = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => false, + }, + }).default; + render(<AdministrationList closeList={() => null} accountBoxItems={[{}]} />); + + expect(screen.getByText('Administration Model List')).to.exist; + expect(screen.queryByText('Apps Model List')).to.not.exist; + expect(screen.queryByText('Settings Model List')).to.not.exist; + expect(screen.queryByText('Audit Model List')).to.not.exist; + }); + + it('should render apps model list when has app account box item', async () => { + const AdministrationList = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '../../../app/ui-utils/client/lib/AccountBox': { + 'AccountBoxItem': {}, + 'isAppAccountBoxItem': () => true, + '@noCallThru': true, + }, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => false, + }, + }).default; + render(<AdministrationList closeList={() => null} accountBoxItems={[{}]} />); + + expect(screen.getByText('Apps Model List')).to.exist; + expect(screen.queryByText('Administration Model List')).to.not.exist; + expect(screen.queryByText('Settings Model List')).to.not.exist; + expect(screen.queryByText('Audit Model List')).to.not.exist; + }); +}); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx new file mode 100644 index 00000000000..cd22ff963c4 --- /dev/null +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx @@ -0,0 +1,161 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import proxyquire from 'proxyquire'; +import React from 'react'; + +import RouterContextMock from '../../../../mocks/client/RouterContextMock'; + +const COMPONENT_PATH = '../../../../../client/components/AdministrationList/AdministrationModelList'; +const defaultConfig = { + '../../../app/ui-utils/client': { + 'SideNav': {}, + '@noCallThru': true, + }, + 'meteor/kadira:flow-router': { + 'FlowRouter': {}, + '@noCallThru': true, + }, + '../../views/hooks/useUpgradeTabParams': { + 'useUpgradeTabParams': () => ({ + isLoading: false, + tabType: 'Upgrade', + trialEndDate: '2020-01-01', + }), + '@noCallThru': true, + }, + '../../../lib/upgradeTab': { + getUpgradeTabLabel: () => 'Upgrade', + isFullyFeature: () => true, + }, + '../../../app/authorization/client': { + 'userHasAllPermission': () => true, + '@noCallThru': true, + }, +}; + +describe('components/AdministrationList/AdministrationModelList', () => { + it('should render administration', async () => { + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render(<AdministrationModelList closeList={() => null} accountBoxItems={[]} showAdmin={true} />); + + expect(screen.getByText('Administration')).to.exist; + expect(screen.getByText('Manage_workspace')).to.exist; + expect(screen.getByText('Upgrade')).to.exist; + }); + + it('should not render workspace and upgrade when does not have permission', async () => { + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render(<AdministrationModelList closeList={() => null} accountBoxItems={[]} showAdmin={false} />); + + expect(screen.getByText('Administration')).to.exist; + expect(screen.queryByText('Manage_workspace')).to.not.exist; + expect(screen.queryByText('Upgrade')).to.not.exist; + }); + + context('when clicked', () => { + it('should go to admin info', async () => { + const pushRoute = spy(); + const closeList = spy(); + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AdministrationModelList closeList={closeList} accountBoxItems={[]} showAdmin={true} /> + </RouterContextMock>, + ); + const button = screen.getByText('Manage_workspace'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-info')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should go to admin index if no permission', async () => { + const pushRoute = spy(); + const closeList = spy(); + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '../../../app/authorization/client': { + 'userHasAllPermission': () => false, + '@noCallThru': true, + }, + }).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AdministrationModelList closeList={closeList} accountBoxItems={[]} showAdmin={true} /> + </RouterContextMock>, + ); + const button = screen.getByText('Manage_workspace'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-index')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should call upgrade route', async () => { + const closeList = spy(); + const pushRoute = spy(); + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AdministrationModelList closeList={closeList} accountBoxItems={[]} showAdmin={true} /> + </RouterContextMock>, + ); + const button = screen.getByText('Upgrade'); + + userEvent.click(button); + + await waitFor(() => expect(pushRoute).to.have.been.called.with('upgrade', { type: 'Upgrade' }, { trialEndDate: '2020-01-01' })); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should render admin box and call router', async () => { + const router = spy(); + const closeList = spy(); + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + 'meteor/kadira:flow-router': { + 'FlowRouter': { + go: router, + }, + '@noCallThru': true, + }, + }).default; + + render( + <AdministrationModelList closeList={closeList} accountBoxItems={[{ name: 'Admin Item', href: 'admin-item' }]} showAdmin={true} />, + ); + + const button = screen.getByText('Admin Item'); + + userEvent.click(button); + await waitFor(() => expect(router).to.have.been.called.with('admin-item')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should render admin box and call sidenav', async () => { + const closeList = spy(); + const setFlex = spy(); + const openFlex = spy(); + const AdministrationModelList = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '../../../app/ui-utils/client': { + 'SideNav': { + setFlex, + openFlex, + }, + '@noCallThru': true, + }, + }).default; + render( + <AdministrationModelList closeList={closeList} accountBoxItems={[{ name: 'Admin Item', sideNav: 'admin' }]} showAdmin={true} />, + ); + const button = screen.getByText('Admin Item'); + + userEvent.click(button); + await waitFor(() => expect(setFlex).to.have.been.called.with('admin')); + await waitFor(() => expect(openFlex).to.have.been.called()); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx new file mode 100644 index 00000000000..8b97900ee1a --- /dev/null +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx @@ -0,0 +1,97 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import proxyquire from 'proxyquire'; +import React from 'react'; + +import RouterContextMock from '../../../../mocks/client/RouterContextMock'; + +const COMPONENT_PATH = '../../../../../client/components/AdministrationList/AppsModelList'; +const defaultConfig = { + '../../../app/ui-message/client/ActionManager': { + 'triggerActionButtonAction': {}, + '@noCallThru': true, + }, +}; + +describe('components/AdministrationList/AppsModelList', () => { + it('should render apps', async () => { + const AppsModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render(<AppsModelList showManageApps={true} closeList={() => null} appBoxItems={[]} />); + + expect(screen.getByText('Apps')).to.exist; + expect(screen.getByText('Marketplace')).to.exist; + expect(screen.getByText('Installed')).to.exist; + }); + + it('should not render marketplace and installed when does not have permission', async () => { + const AppsModelList = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: (): boolean => false, + }, + }).default; + render(<AppsModelList showManageApps={false} closeList={() => null} appBoxItems={[]} />); + + expect(screen.getByText('Apps')).to.exist; + expect(screen.queryByText('Marketplace')).to.not.exist; + expect(screen.queryByText('Installed')).to.not.exist; + }); + + context('when clicked', () => { + it('should go to admin marketplace', async () => { + const pushRoute = spy(); + const closeList = spy(); + const AppsModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AppsModelList showManageApps={true} closeList={closeList} appBoxItems={[]} /> + </RouterContextMock>, + ); + const button = screen.getByText('Marketplace'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-marketplace')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should go to installed', async () => { + const pushRoute = spy(); + const closeList = spy(); + const AppsModelList = proxyquire.load(COMPONENT_PATH, defaultConfig).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AppsModelList showManageApps={true} closeList={closeList} appBoxItems={[]} /> + </RouterContextMock>, + ); + const button = screen.getByText('Installed'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-marketplace', { context: 'installed' })); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should render apps and trigger action', async () => { + const pushRoute = spy(); + const closeList = spy(); + const triggerActionButtonAction = spy(); + const AppsModelList = proxyquire.load(COMPONENT_PATH, { + ...defaultConfig, + '../../../app/ui-message/client/ActionManager': { + triggerActionButtonAction, + '@noCallThru': true, + }, + }).default; + render( + <RouterContextMock pushRoute={pushRoute}> + <AppsModelList showManageApps={true} closeList={closeList} appBoxItems={[{ name: 'Custom App' }]} /> + </RouterContextMock>, + ); + const button = screen.getByText('Custom App'); + + userEvent.click(button); + await waitFor(() => expect(triggerActionButtonAction).to.have.been.called()); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AuditModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AuditModelList.spec.tsx new file mode 100644 index 00000000000..2109a65d356 --- /dev/null +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AuditModelList.spec.tsx @@ -0,0 +1,57 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import React from 'react'; + +import AuditModelList from '../../../../../client/components/AdministrationList/AuditModelList'; +import RouterContextMock from '../../../../mocks/client/RouterContextMock'; + +describe('components/AdministrationList/AuditModelList', () => { + it('should render audit', async () => { + render(<AuditModelList showAudit={true} showAuditLog={true} closeList={() => null} />); + + expect(screen.getByText('Audit')).to.exist; + expect(screen.getByText('Messages')).to.exist; + expect(screen.getByText('Logs')).to.exist; + }); + + it('should not render messages and log when does not have permission', async () => { + render(<AuditModelList showAudit={false} showAuditLog={false} closeList={() => null} />); + + expect(screen.getByText('Audit')).to.exist; + expect(screen.queryByText('Messages')).to.not.exist; + expect(screen.queryByText('Logs')).to.not.exist; + }); + + context('when clicked', () => { + it('should go to audit home', async () => { + const pushRoute = spy(); + const closeList = spy(); + render( + <RouterContextMock pushRoute={pushRoute}> + <AuditModelList showAudit={true} showAuditLog={false} closeList={closeList} /> + </RouterContextMock>, + ); + const button = screen.getByText('Messages'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('audit-home')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + + it('should go to audit log', async () => { + const pushRoute = spy(); + const closeList = spy(); + render( + <RouterContextMock pushRoute={pushRoute}> + <AuditModelList showAudit={false} showAuditLog={true} closeList={closeList} /> + </RouterContextMock>, + ); + const button = screen.getByText('Logs'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('audit-log')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx new file mode 100644 index 00000000000..5d711b4269a --- /dev/null +++ b/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx @@ -0,0 +1,32 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import React from 'react'; + +import SettingsModelList from '../../../../../client/components/AdministrationList/SettingsModelList'; +import RouterContextMock from '../../../../mocks/client/RouterContextMock'; + +describe('components/AdministrationList/SettingsModelList', () => { + it('should render', async () => { + render(<SettingsModelList closeList={() => null} />); + + expect(screen.getByText('Workspace_settings')).to.exist; + }); + + context('when clicked', () => { + it('should go to admin settings', async () => { + const pushRoute = spy(); + const closeList = spy(); + render( + <RouterContextMock pushRoute={pushRoute}> + <SettingsModelList closeList={closeList} /> + </RouterContextMock>, + ); + const button = screen.getByText('Workspace_settings'); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-settings')); + await waitFor(() => expect(closeList).to.have.been.called()); + }); + }); +}); diff --git a/apps/meteor/tests/unit/client/siderbar/header/actions/Administration.spec.tsx b/apps/meteor/tests/unit/client/siderbar/header/actions/Administration.spec.tsx new file mode 100644 index 00000000000..c60f4eb3946 --- /dev/null +++ b/apps/meteor/tests/unit/client/siderbar/header/actions/Administration.spec.tsx @@ -0,0 +1,114 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import proxyquire from 'proxyquire'; +import React from 'react'; + +const COMPONENT_PATH = '../../../../../../client/sidebar/header/actions/Administration'; +const defaultConfig = { + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => true, + }, + '@rocket.chat/fuselage-hooks': { + useMutableCallback: (cb: () => null) => cb(), + }, + '../../../../ee/client/hooks/useHasLicenseModule': { + useHasLicenseModule: () => true, + }, + '../../../../app/ui-utils/client': { + AccountBox: { + getItems: () => [], + }, + }, + '../../../hooks/useReactiveValue': { + useReactiveValue: (item: unknown) => item, + }, + '../hooks/useDropdownVisibility': { + useDropdownVisibility: () => ({ + isVisible: true, + toggle: () => null, + }), + }, + '../../../components/AdministrationList/AdministrationList': () => <p>Administration List</p>, +}; + +describe('sidebar/header/actions/Administration', () => { + it('should render administration list', async () => { + const Administration = proxyquire.noCallThru().load(COMPONENT_PATH, defaultConfig).default; + + render(<Administration />); + + expect(screen.getByRole('button')).to.exist; + expect(screen.getByText('Administration List')).to.exist; + }); + + it('should not render administration list when isVisible false', async () => { + const Administration = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '../hooks/useDropdownVisibility': { + useDropdownVisibility: () => ({ + isVisible: false, + toggle: () => null, + }), + }, + }).default; + + render(<Administration />); + + expect(screen.getByRole('button')).to.exist; + expect(screen.queryByText('Administration List')).to.not.exist; + }); + + it('should render button if has accountBoxItem', async () => { + const Administration = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '../../../../app/ui-utils/client': { + AccountBox: { + getItems: () => [{}], + }, + }, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => false, + }, + }).default; + + render(<Administration />); + + expect(screen.getByRole('button')).to.exist; + expect(screen.getByText('Administration List')).to.exist; + }); + + it('should not render button if does not have permission', async () => { + const Administration = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '@rocket.chat/ui-contexts': { + useAtLeastOnePermission: () => false, + }, + }).default; + + render(<Administration />); + + expect(screen.queryByText('button')).to.not.exist; + expect(screen.getByText('Administration List')).to.exist; + }); + + context('when clicked', () => { + it('should toggle dropdown', async () => { + const toggle = spy(); + const Administration = proxyquire.noCallThru().load(COMPONENT_PATH, { + ...defaultConfig, + '../hooks/useDropdownVisibility': { + useDropdownVisibility: () => ({ + isVisible: true, + toggle, + }), + }, + }).default; + + render(<Administration />); + + userEvent.click(screen.getByRole('button')); + await waitFor(() => expect(toggle).to.have.been.called()); + }); + }); +}); diff --git a/apps/meteor/tests/unit/lib/getUpgradeTabType.spec.ts b/apps/meteor/tests/unit/lib/upgradeTab.spec.ts similarity index 53% rename from apps/meteor/tests/unit/lib/getUpgradeTabType.spec.ts rename to apps/meteor/tests/unit/lib/upgradeTab.spec.ts index 917272beae5..42e2ec7b385 100644 --- a/apps/meteor/tests/unit/lib/getUpgradeTabType.spec.ts +++ b/apps/meteor/tests/unit/lib/upgradeTab.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { getUpgradeTabType } from '../../../lib/getUpgradeTabType'; +import { getUpgradeTabLabel, getUpgradeTabType, isFullyFeature } from '../../../lib/upgradeTab'; describe('getUpgradeTabType()', () => { it("should return 'go-fully-featured'", () => { @@ -11,7 +11,6 @@ describe('getUpgradeTabType()', () => { hasValidLicense: false, isTrial: false, hadExpiredTrials: false, - hasGoldLicense: false, }), ).to.be.equal('go-fully-featured'); }); @@ -23,7 +22,6 @@ describe('getUpgradeTabType()', () => { hasValidLicense: false, isTrial: false, hadExpiredTrials: false, - hasGoldLicense: false, }), ).to.be.equal('go-fully-featured-registered'); }); @@ -35,7 +33,6 @@ describe('getUpgradeTabType()', () => { hasValidLicense: false, isTrial: false, hadExpiredTrials: true, - hasGoldLicense: false, }), ).to.be.equal('upgrade-your-plan'); }); @@ -47,23 +44,10 @@ describe('getUpgradeTabType()', () => { hasValidLicense: true, isTrial: true, hadExpiredTrials: false, - hasGoldLicense: false, }), ).to.be.equal('trial-enterprise'); }); - it("should return 'trial-gold'", () => { - expect( - getUpgradeTabType({ - registered: true, - hasValidLicense: true, - isTrial: true, - hadExpiredTrials: false, - hasGoldLicense: true, - }), - ).to.be.equal('trial-gold'); - }); - it('should return false', () => { expect( getUpgradeTabType({ @@ -71,8 +55,34 @@ describe('getUpgradeTabType()', () => { hasValidLicense: true, isTrial: false, hadExpiredTrials: false, - hasGoldLicense: false, }), ).to.be.equal(false); }); }); + +describe('getUpgradeTabLabel()', () => { + it("should return 'Upgrade_tab_go_fully_featured'", () => { + expect(getUpgradeTabLabel('go-fully-featured')).to.be.equal('Upgrade_tab_go_fully_featured'); + expect(getUpgradeTabLabel('go-fully-featured-registered')).to.be.equal('Upgrade_tab_go_fully_featured'); + }); + + it("should return 'Upgrade_tab_trial_guide'", () => { + expect(getUpgradeTabLabel('trial-enterprise')).to.be.equal('Upgrade_tab_trial_guide'); + }); + + it("should return 'Upgrade_tab_upgrade_your_plan'", () => { + expect(getUpgradeTabLabel('upgrade-your-plan')).to.be.equal('Upgrade_tab_upgrade_your_plan'); + expect(getUpgradeTabLabel(false)).to.be.equal('Upgrade_tab_upgrade_your_plan'); + }); +}); + +describe('isFullyFeature()', () => { + it("should return 'true", () => { + expect(isFullyFeature('go-fully-featured')).to.be.equal(true); + }); + + it("should return 'false", () => { + expect(isFullyFeature('upgrade-your-plan')).to.be.equal(false); + expect(isFullyFeature(false)).to.be.equal(false); + }); +}); -- GitLab From ab3ed80a6d3bf677bd597b4ca0103cd28bc1530e Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi <marcos.defendi@rocket.chat> Date: Fri, 23 Sep 2022 19:56:40 -0300 Subject: [PATCH 073/107] [NEW] Matrix Federation events coverage expansion (support for 5 more events) (#26705) --- .../client/discussionFromMessageBox.js | 11 +- .../app/federation-v2/server/Federation.ts | 9 + .../application/AbstractFederationService.ts | 19 +- .../application/MessageServiceListener.ts | 57 ++++ .../application/RoomRedactionHandlers.ts | 61 ++++ .../server/application/RoomServiceListener.ts | 84 ++++- .../application/input/MessageReceiverDto.ts | 31 ++ .../application/input/RoomReceiverDto.ts | 170 +++++++++-- .../application/sender/MessageSenders.ts | 49 +++ .../sender/MessageServiceSender.ts | 98 ++++++ .../application/sender/RoomServiceSender.ts | 72 ++++- .../server/domain/FederatedRoom.ts | 6 +- .../server/domain/IFederationBridge.ts | 12 + apps/meteor/app/federation-v2/server/index.ts | 46 ++- .../server/infrastructure/Factory.ts | 65 +++- .../server/infrastructure/matrix/Bridge.ts | 127 +++++++- .../matrix/converters/MessageReceiver.ts | 21 ++ .../matrix/converters/RoomReceiver.ts | 61 +++- .../matrix/definitions/AbstractMatrixEvent.ts | 2 + .../matrix/definitions/MatrixEventType.ts | 2 + .../definitions/events/MessageReacted.ts | 17 ++ .../definitions/events/RoomEventRedacted.ts | 11 + .../definitions/events/RoomMessageSent.ts | 42 ++- .../infrastructure/matrix/handlers/Message.ts | 17 ++ .../infrastructure/matrix/handlers/Room.ts | 49 ++- .../infrastructure/matrix/handlers/index.ts | 6 +- .../rocket-chat/adapters/File.ts | 64 ++++ .../rocket-chat/adapters/Message.ts | 96 +++++- .../rocket-chat/adapters/MessageConverter.ts | 7 + .../rocket-chat/adapters/User.ts | 4 + .../infrastructure/rocket-chat/hooks/index.ts | 66 +++- .../app/file-upload/server/lib/FileUpload.js | 4 + .../server/methods/sendFileMessage.ts | 196 ++++++------ .../app/lib/server/functions/deleteMessage.ts | 2 +- apps/meteor/app/reactions/client/init.js | 2 +- .../app/reactions/server/setReaction.js | 3 +- .../client/imports/components/message-box.css | 4 - .../client/messageBox/messageBox.html | 40 ++- .../client/messageBox/messageBox.ts | 1 + .../client/messageBox/messageBoxActions.ts | 10 +- .../app/ui-utils/client/lib/MessageAction.ts | 2 +- .../client/lib/messageActionDefault.ts | 13 +- apps/meteor/app/ui/client/lib/fileUpload.ts | 4 + .../app/ui/client/views/app/lib/dropzone.ts | 12 +- .../body/useFileUploadDropTarget.ts | 13 +- .../views/room/lib/Toolbox/defaultActions.ts | 23 +- .../FileUploadModal/FileUploadModal.tsx | 16 +- .../application/input/RoomReceiverDto.ts | 43 --- .../application/sender/RoomServiceSender.ts | 23 -- .../DMRoomInternalHooksServiceSender.ts | 14 +- .../RoomInternalHooksServiceSender.ts | 18 +- .../sender/room/RoomServiceSender.ts | 25 ++ .../ee/app/federation-v2/server/index.ts | 13 +- .../server/infrastructure/Factory.ts | 20 +- .../infrastructure/rocket-chat/hooks/index.ts | 7 +- .../DMRoomInternalHooksServiceSender.spec.ts | 5 +- .../RoomInternalHooksServiceSender.spec.ts | 13 +- .../rocket-chat/hooks/hooks.spec.ts | 95 +++--- apps/meteor/lib/callbacks.ts | 5 + apps/meteor/package.json | 1 + .../rocketchat-i18n/i18n/de.i18n.json | 2 - .../rocketchat-i18n/i18n/en.i18n.json | 3 - apps/meteor/server/models/raw/Messages.ts | 77 ++++- .../server/unit/Federation.spec.ts | 12 + .../MessageServiceListener.spec.ts | 155 ++++++++++ .../application/RoomServiceListener.spec.ts | 231 ++++++++++++++ .../application/sender/MessageSenders.spec.ts | 91 ++++++ .../sender/MessageServiceSender.spec.ts | 286 ++++++++++++++++++ .../sender/RoomServiceSender.spec.ts | 189 +++++++++++- .../server/unit/domain/FederatedRoom.spec.ts | 10 + .../unit/infrastructure/matrix/Bridge.spec.ts | 10 +- .../matrix/converters/RoomReceiver.spec.ts | 104 ++++++- .../matrix/handlers/Room.spec.ts | 55 ++++ .../core-typings/src/IMessage/IMessage.ts | 9 +- packages/core-typings/src/IUpload.ts | 8 + .../src/models/IMessagesModel.ts | 8 + yarn.lock | 10 +- 77 files changed, 2883 insertions(+), 386 deletions(-) create mode 100644 apps/meteor/app/federation-v2/server/application/MessageServiceListener.ts create mode 100644 apps/meteor/app/federation-v2/server/application/RoomRedactionHandlers.ts create mode 100644 apps/meteor/app/federation-v2/server/application/input/MessageReceiverDto.ts create mode 100644 apps/meteor/app/federation-v2/server/application/sender/MessageSenders.ts create mode 100644 apps/meteor/app/federation-v2/server/application/sender/MessageServiceSender.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/MessageReceiver.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/MessageReacted.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomEventRedacted.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Message.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/File.ts create mode 100644 apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/MessageConverter.ts delete mode 100644 apps/meteor/ee/app/federation-v2/server/application/input/RoomReceiverDto.ts delete mode 100644 apps/meteor/ee/app/federation-v2/server/application/sender/RoomServiceSender.ts rename apps/meteor/ee/app/federation-v2/server/application/sender/{ => room}/DMRoomInternalHooksServiceSender.ts (90%) rename apps/meteor/ee/app/federation-v2/server/application/sender/{ => room}/RoomInternalHooksServiceSender.ts (90%) create mode 100644 apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomServiceSender.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/server/unit/application/MessageServiceListener.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageSenders.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageServiceSender.spec.ts create mode 100644 apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/handlers/Room.spec.ts diff --git a/apps/meteor/app/discussion/client/discussionFromMessageBox.js b/apps/meteor/app/discussion/client/discussionFromMessageBox.js index eb2d67e53dc..56cf96c056f 100644 --- a/apps/meteor/app/discussion/client/discussionFromMessageBox.js +++ b/apps/meteor/app/discussion/client/discussionFromMessageBox.js @@ -1,11 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { Session } from 'meteor/session'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { messageBox } from '../../ui-utils/client'; import { settings } from '../../settings/client'; import { hasPermission } from '../../authorization/client'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import CreateDiscussion from '../../../client/components/CreateDiscussion/CreateDiscussion'; +import { Rooms } from '../../models/client'; Meteor.startup(function () { Tracker.autorun(() => { @@ -15,7 +18,13 @@ Meteor.startup(function () { messageBox.actions.add('Create_new', 'Discussion', { id: 'start-discussion', icon: 'discussion', - condition: () => hasPermission('start-discussion') || hasPermission('start-discussion-other-user'), + condition: () => { + const room = Rooms.findOne(Session.get('openedRoom')); + if (!room) { + return false; + } + return (hasPermission('start-discussion') || hasPermission('start-discussion-other-user')) && !isRoomFederated(room); + }, action(data) { imperativeModal.open({ component: CreateDiscussion, diff --git a/apps/meteor/app/federation-v2/server/Federation.ts b/apps/meteor/app/federation-v2/server/Federation.ts index 25080150b68..7c856ebe1d8 100644 --- a/apps/meteor/app/federation-v2/server/Federation.ts +++ b/apps/meteor/app/federation-v2/server/Federation.ts @@ -2,6 +2,7 @@ import type { IRoom, ValueOf } from '@rocket.chat/core-typings'; import { isDirectMessageRoom } from '@rocket.chat/core-typings'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; +import { escapeExternalFederationEventId, unescapeExternalFederationEventId } from './infrastructure/rocket-chat/adapters/MessageConverter'; const allowedActionsInFederatedRooms: ValueOf<typeof RoomMemberActions>[] = [ RoomMemberActions.REMOVE_USER, @@ -18,4 +19,12 @@ export class Federation { public static isAFederatedUsername(username: string): boolean { return username.includes('@') && username.includes(':'); } + + public static escapeExternalFederationEventId(externalEventId: string): string { + return escapeExternalFederationEventId(externalEventId); + } + + public static unescapeExternalFederationEventId(externalEventId: string): string { + return unescapeExternalFederationEventId(externalEventId); + } } diff --git a/apps/meteor/app/federation-v2/server/application/AbstractFederationService.ts b/apps/meteor/app/federation-v2/server/application/AbstractFederationService.ts index 773eb480afc..a210b45c377 100644 --- a/apps/meteor/app/federation-v2/server/application/AbstractFederationService.ts +++ b/apps/meteor/app/federation-v2/server/application/AbstractFederationService.ts @@ -20,14 +20,19 @@ export abstract class FederationService { existsOnlyOnProxyServer = false, providedName?: string, ): Promise<void> { + const internalUser = await this.internalUserAdapter.getInternalUserByUsername(username); const externalUserProfileInformation = await this.bridge.getUserProfileInformation(externalUserId); - const name = externalUserProfileInformation?.displayName || providedName || username; - const federatedUser = FederatedUser.createInstance(externalUserId, { - name, - username, - existsOnlyOnProxyServer, - }); - + let federatedUser; + if (internalUser) { + federatedUser = FederatedUser.createWithInternalReference(externalUserId, existsOnlyOnProxyServer, internalUser); + } else { + const name = externalUserProfileInformation?.displayName || providedName || username; + federatedUser = FederatedUser.createInstance(externalUserId, { + name, + username, + existsOnlyOnProxyServer, + }); + } await this.internalUserAdapter.createFederatedUser(federatedUser); } diff --git a/apps/meteor/app/federation-v2/server/application/MessageServiceListener.ts b/apps/meteor/app/federation-v2/server/application/MessageServiceListener.ts new file mode 100644 index 00000000000..4a0a1847ad0 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/MessageServiceListener.ts @@ -0,0 +1,57 @@ +import { isMessageFromMatrixFederation } from '@rocket.chat/core-typings'; + +import type { IFederationBridge } from '../domain/IFederationBridge'; +import type { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; +import type { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import type { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import type { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { FederationService } from './AbstractFederationService'; +import type { FederationMessageReactionEventDto } from './input/MessageReceiverDto'; + +export class FederationMessageServiceListener extends FederationService { + constructor( + protected internalRoomAdapter: RocketChatRoomAdapter, + protected internalUserAdapter: RocketChatUserAdapter, + protected internalMessageAdapter: RocketChatMessageAdapter, + protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected bridge: IFederationBridge, + ) { + super(bridge, internalUserAdapter, internalSettingsAdapter); + } + + public async onMessageReaction(messageReactionEventInput: FederationMessageReactionEventDto): Promise<void> { + const { + externalRoomId, + emoji, + externalSenderId, + externalEventId: externalReactionEventId, + externalReactedEventId: externalMessageId, + } = messageReactionEventInput; + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const federatedUser = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!federatedUser) { + return; + } + const message = await this.internalMessageAdapter.getMessageByFederationId(externalMessageId); + if (!message) { + return; + } + if (!isMessageFromMatrixFederation(message)) { + return; + } + // TODO: move this to a Message entity in the domain layer + const userAlreadyReacted = Boolean( + federatedUser.getUsername() && message.reactions?.[emoji]?.usernames?.includes(federatedUser.getUsername() as string), + ); + if (userAlreadyReacted) { + return; + } + + await this.internalMessageAdapter.reactToMessage(federatedUser, message, emoji, externalReactionEventId); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/RoomRedactionHandlers.ts b/apps/meteor/app/federation-v2/server/application/RoomRedactionHandlers.ts new file mode 100644 index 00000000000..84c5fb0937a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomRedactionHandlers.ts @@ -0,0 +1,61 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import type { FederatedUser } from '../domain/FederatedUser'; +import { Federation } from '../Federation'; +import type { RocketChatMessageAdapter } from '../infrastructure/rocket-chat/adapters/Message'; + +export interface IRoomRedactionHandlers { + handle(): Promise<void>; +} + +class DeleteMessageHandler implements IRoomRedactionHandlers { + constructor( + private readonly internalMessageAdapter: RocketChatMessageAdapter, + private readonly message: IMessage, + private readonly federatedUser: FederatedUser, + ) {} + + public async handle(): Promise<void> { + await this.internalMessageAdapter.deleteMessage(this.message, this.federatedUser); + } +} + +class UnreactToMessageHandler implements IRoomRedactionHandlers { + constructor( + private readonly internalMessageAdapter: RocketChatMessageAdapter, + private readonly message: IMessage, + private readonly federatedUser: FederatedUser, + private readonly redactsEvents: string, + ) {} + + public async handle(): Promise<void> { + const normalizedEventId = Federation.escapeExternalFederationEventId(this.redactsEvents); + const reaction = Object.keys(this.message.reactions || {}).find( + (key) => + this.message.reactions?.[key]?.federationReactionEventIds?.[normalizedEventId] === this.federatedUser.getUsername() && + this.message.reactions?.[key]?.usernames?.includes(this.federatedUser.getUsername() || ''), + ); + if (!reaction) { + return; + } + await this.internalMessageAdapter.unreactToMessage(this.federatedUser, this.message, reaction, this.redactsEvents); + } +} + +export const getRedactMessageHandler = async ( + internalMessageAdapter: RocketChatMessageAdapter, + redactsEvent: string, + federatedUser: FederatedUser, +): Promise<IRoomRedactionHandlers | undefined> => { + const message = await internalMessageAdapter.getMessageByFederationId(redactsEvent); + const messageWithReaction = await internalMessageAdapter.findOneByFederationIdOnReactions(redactsEvent, federatedUser); + if (!message && !messageWithReaction) { + return; + } + if (messageWithReaction) { + return new UnreactToMessageHandler(internalMessageAdapter, messageWithReaction, federatedUser, redactsEvent); + } + if (message) { + return new DeleteMessageHandler(internalMessageAdapter, message, federatedUser); + } +}; diff --git a/apps/meteor/app/federation-v2/server/application/RoomServiceListener.ts b/apps/meteor/app/federation-v2/server/application/RoomServiceListener.ts index afdd395e565..24980387a1a 100644 --- a/apps/meteor/app/federation-v2/server/application/RoomServiceListener.ts +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceListener.ts @@ -16,8 +16,13 @@ import type { FederationRoomChangeJoinRulesDto, FederationRoomChangeNameDto, FederationRoomChangeTopicDto, + FederationRoomReceiveExternalFileMessageDto, + FederationRoomRedactEventDto, + FederationRoomEditExternalMessageDto, } from './input/RoomReceiverDto'; import { FederationService } from './AbstractFederationService'; +import type { RocketChatFileAdapter } from '../infrastructure/rocket-chat/adapters/File'; +import { getRedactMessageHandler } from './RoomRedactionHandlers'; export class FederationRoomServiceListener extends FederationService { constructor( @@ -25,6 +30,7 @@ export class FederationRoomServiceListener extends FederationService { protected internalUserAdapter: RocketChatUserAdapter, protected internalMessageAdapter: RocketChatMessageAdapter, protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected internalFileAdapter: RocketChatFileAdapter, protected bridge: IFederationBridge, ) { super(bridge, internalUserAdapter, internalSettingsAdapter); @@ -180,7 +186,7 @@ export class FederationRoomServiceListener extends FederationService { } public async onExternalMessageReceived(roomReceiveExternalMessageInput: FederationRoomReceiveExternalMessageDto): Promise<void> { - const { externalRoomId, externalSenderId, messageText } = roomReceiveExternalMessageInput; + const { externalRoomId, externalSenderId, messageText, externalEventId } = roomReceiveExternalMessageInput; const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); if (!federatedRoom) { @@ -192,7 +198,62 @@ export class FederationRoomServiceListener extends FederationService { return; } - await this.internalMessageAdapter.sendMessage(senderUser, federatedRoom, messageText); + await this.internalMessageAdapter.sendMessage(senderUser, federatedRoom, messageText, externalEventId); + } + + public async onExternalMessageEditedReceived(roomEditExternalMessageInput: FederationRoomEditExternalMessageDto): Promise<void> { + const { externalRoomId, externalSenderId, editsEvent, newMessageText } = roomEditExternalMessageInput; + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const senderUser = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!senderUser) { + return; + } + + const message = await this.internalMessageAdapter.getMessageByFederationId(editsEvent); + if (!message) { + return; + } + // TODO: create an entity to abstract all the message logic + if (!FederatedRoom.shouldUpdateMessage(newMessageText, message)) { + return; + } + + await this.internalMessageAdapter.editMessage(senderUser, newMessageText, message); + } + + public async onExternalFileMessageReceived(roomReceiveExternalMessageInput: FederationRoomReceiveExternalFileMessageDto): Promise<void> { + const { externalRoomId, externalSenderId, messageBody, externalEventId } = roomReceiveExternalMessageInput; + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const senderUser = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!senderUser) { + return; + } + const fileDetails = { + name: messageBody.filename, + size: messageBody.size, + type: messageBody.mimetype, + rid: federatedRoom.getInternalId(), + userId: senderUser.getInternalId(), + }; + const readableStream = await this.bridge.getReadStreamForFileFromUrl(senderUser.getExternalId(), messageBody.url); + const { files = [], attachments } = await this.internalFileAdapter.uploadFile( + readableStream, + federatedRoom.getInternalId(), + senderUser.getInternalReference(), + fileDetails, + ); + + await this.internalMessageAdapter.sendFileMessage(senderUser, federatedRoom, files, attachments, externalEventId); } public async onChangeJoinRules(roomJoinRulesChangeInput: FederationRoomChangeJoinRulesDto): Promise<void> { @@ -255,4 +316,23 @@ export class FederationRoomServiceListener extends FederationService { await this.internalRoomAdapter.updateRoomTopic(federatedRoom, federatedUser); } + + public async onRedactEvent(roomRedactEventInput: FederationRoomRedactEventDto): Promise<void> { + const { externalRoomId, redactsEvent, externalSenderId } = roomRedactEventInput; + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + const federatedUser = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!federatedUser) { + return; + } + const handler = await getRedactMessageHandler(this.internalMessageAdapter, redactsEvent, federatedUser); + if (!handler) { + return; + } + await handler.handle(); + } } diff --git a/apps/meteor/app/federation-v2/server/application/input/MessageReceiverDto.ts b/apps/meteor/app/federation-v2/server/application/input/MessageReceiverDto.ts new file mode 100644 index 00000000000..43dc255fef9 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/MessageReceiverDto.ts @@ -0,0 +1,31 @@ +import type { IFederationReceiverBaseRoomInputDto } from './RoomReceiverDto'; +import { FederationBaseRoomInputDto } from './RoomReceiverDto'; + +interface IFederationRoomMessageReactionInputDto extends IFederationReceiverBaseRoomInputDto { + externalSenderId: string; + externalEventId: string; + externalReactedEventId: string; + emoji: string; +} + +export class FederationMessageReactionEventDto extends FederationBaseRoomInputDto { + constructor({ + externalRoomId, + normalizedRoomId, + externalEventId, + externalReactedEventId, + emoji, + externalSenderId, + }: IFederationRoomMessageReactionInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); + this.emoji = emoji; + this.externalSenderId = externalSenderId; + this.externalReactedEventId = externalReactedEventId; + } + + emoji: string; + + externalSenderId: string; + + externalReactedEventId: string; +} diff --git a/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts index 8e20bbe0c98..5df8bdd7db7 100644 --- a/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts +++ b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts @@ -2,7 +2,11 @@ import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { EVENT_ORIGIN } from '../../domain/IFederationBridge'; -export interface IFederationReceiverBaseRoomInputDto { +interface IFederationBaseInputDto { + externalEventId: string; +} + +export interface IFederationReceiverBaseRoomInputDto extends IFederationBaseInputDto { externalRoomId: string; normalizedRoomId: string; } @@ -29,30 +33,41 @@ export interface IFederationChangeMembershipInputDto extends IFederationReceiver externalRoomName?: string; } -export interface IFederationSendInternalMessageInputDto extends IFederationReceiverBaseRoomInputDto { - externalSenderId: string; - normalizedSenderId: string; - messageText: string; -} - export interface IFederationRoomChangeJoinRulesDtoInputDto extends IFederationReceiverBaseRoomInputDto { roomType: RoomType; } export interface IFederationRoomNameChangeInputDto extends IFederationReceiverBaseRoomInputDto { normalizedRoomName: string; - externalSenderId: string; } export interface IFederationRoomTopicChangeInputDto extends IFederationReceiverBaseRoomInputDto { roomTopic: string; + externalSenderId: string; +} +export interface IFederationRoomRedactEventInputDto extends IFederationReceiverBaseRoomInputDto { + redactsEvent: string; externalSenderId: string; } -export class FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId }: IFederationReceiverBaseRoomInputDto) { +export interface IFederationSendInternalMessageBaseInputDto extends IFederationReceiverBaseRoomInputDto { + externalSenderId: string; + normalizedSenderId: string; +} + +abstract class FederationBaseDto { + constructor({ externalEventId }: { externalEventId: string }) { + this.externalEventId = externalEventId; + } + + externalEventId: string; +} + +export class FederationBaseRoomInputDto extends FederationBaseDto { + constructor({ externalRoomId, normalizedRoomId, externalEventId }: IFederationReceiverBaseRoomInputDto) { + super({ externalEventId }); this.externalRoomId = externalRoomId; this.normalizedRoomId = normalizedRoomId; } @@ -72,8 +87,9 @@ export class FederationRoomCreateInputDto extends FederationBaseRoomInputDto { roomType, externalRoomName, internalRoomId, + externalEventId, }: IFederationCreateInputDto) { - super({ externalRoomId, normalizedRoomId }); + super({ externalRoomId, normalizedRoomId, externalEventId }); this.externalInviterId = externalInviterId; this.normalizedInviterId = normalizedInviterId; this.wasInternallyProgramaticallyCreated = wasInternallyProgramaticallyCreated; @@ -109,8 +125,9 @@ export class FederationRoomChangeMembershipDto extends FederationBaseRoomInputDt leave, roomType, externalRoomName, + externalEventId, }: IFederationChangeMembershipInputDto) { - super({ externalRoomId, normalizedRoomId }); + super({ externalRoomId, normalizedRoomId, externalEventId }); this.externalInviterId = externalInviterId; this.normalizedInviterId = normalizedInviterId; this.externalInviteeId = externalInviteeId; @@ -144,15 +161,28 @@ export class FederationRoomChangeMembershipDto extends FederationBaseRoomInputDt externalRoomName?: string; } -export class FederationRoomReceiveExternalMessageDto extends FederationBaseRoomInputDto { +class ExternalMessageBaseDto extends FederationBaseRoomInputDto { + constructor({ externalRoomId, normalizedRoomId, externalSenderId, normalizedSenderId, externalEventId }: Record<string, any>) { + super({ externalRoomId, normalizedRoomId, externalEventId }); + this.externalSenderId = externalSenderId; + this.normalizedSenderId = normalizedSenderId; + } + + externalSenderId: string; + + normalizedSenderId: string; +} + +export class FederationRoomReceiveExternalMessageDto extends ExternalMessageBaseDto { constructor({ externalRoomId, normalizedRoomId, externalSenderId, normalizedSenderId, messageText, - }: IFederationSendInternalMessageInputDto) { - super({ externalRoomId, normalizedRoomId }); + externalEventId, + }: IFederationSendInternalMessageBaseInputDto & { messageText: string }) { + super({ externalRoomId, normalizedRoomId, externalEventId }); this.externalSenderId = externalSenderId; this.normalizedSenderId = normalizedSenderId; this.messageText = messageText; @@ -165,9 +195,89 @@ export class FederationRoomReceiveExternalMessageDto extends FederationBaseRoomI messageText: string; } +export class FederationRoomEditExternalMessageDto extends ExternalMessageBaseDto { + constructor({ + externalRoomId, + normalizedRoomId, + externalSenderId, + normalizedSenderId, + newMessageText, + editsEvent, + externalEventId, + }: IFederationSendInternalMessageBaseInputDto & { newMessageText: string; editsEvent: string }) { + super({ externalRoomId, normalizedRoomId, externalEventId }); + this.externalSenderId = externalSenderId; + this.normalizedSenderId = normalizedSenderId; + this.newMessageText = newMessageText; + this.editsEvent = editsEvent; + } + + externalSenderId: string; + + normalizedSenderId: string; + + newMessageText: string; + + editsEvent: string; +} + +export interface IFederationFileMessageInputDto { + filename: string; + mimetype: string; + size: number; + messageText: string; + url: string; +} + +class FederationFileMessageInputDto { + constructor({ filename, mimetype, size, messageText, url }: IFederationFileMessageInputDto) { + this.filename = filename; + this.mimetype = mimetype; + this.size = size; + this.messageText = messageText; + this.url = url; + } + + filename: string; + + mimetype: string; + + size: number; + + messageText: string; + + url: string; +} + +export class FederationRoomReceiveExternalFileMessageDto extends ExternalMessageBaseDto { + constructor({ + externalRoomId, + normalizedRoomId, + externalSenderId, + normalizedSenderId, + filename, + mimetype, + size, + messageText, + url, + externalEventId, + }: IFederationSendInternalMessageBaseInputDto & IFederationFileMessageInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); + this.externalSenderId = externalSenderId; + this.normalizedSenderId = normalizedSenderId; + this.messageBody = new FederationFileMessageInputDto({ filename, mimetype, size, messageText, url }); + } + + externalSenderId: string; + + normalizedSenderId: string; + + messageBody: FederationFileMessageInputDto; +} + export class FederationRoomChangeJoinRulesDto extends FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId, roomType }: IFederationRoomChangeJoinRulesDtoInputDto) { - super({ externalRoomId, normalizedRoomId }); + constructor({ externalRoomId, normalizedRoomId, roomType, externalEventId }: IFederationRoomChangeJoinRulesDtoInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); this.roomType = roomType; } @@ -175,8 +285,14 @@ export class FederationRoomChangeJoinRulesDto extends FederationBaseRoomInputDto } export class FederationRoomChangeNameDto extends FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId, normalizedRoomName, externalSenderId }: IFederationRoomNameChangeInputDto) { - super({ externalRoomId, normalizedRoomId }); + constructor({ + externalRoomId, + normalizedRoomId, + normalizedRoomName, + externalSenderId, + externalEventId, + }: IFederationRoomNameChangeInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); this.normalizedRoomName = normalizedRoomName; this.externalSenderId = externalSenderId; } @@ -187,8 +303,8 @@ export class FederationRoomChangeNameDto extends FederationBaseRoomInputDto { } export class FederationRoomChangeTopicDto extends FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId, roomTopic, externalSenderId }: IFederationRoomTopicChangeInputDto) { - super({ externalRoomId, normalizedRoomId }); + constructor({ externalRoomId, normalizedRoomId, roomTopic, externalSenderId, externalEventId }: IFederationRoomTopicChangeInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); this.roomTopic = roomTopic; this.externalSenderId = externalSenderId; } @@ -197,3 +313,15 @@ export class FederationRoomChangeTopicDto extends FederationBaseRoomInputDto { externalSenderId: string; } + +export class FederationRoomRedactEventDto extends FederationBaseRoomInputDto { + constructor({ externalRoomId, normalizedRoomId, externalEventId, redactsEvent, externalSenderId }: IFederationRoomRedactEventInputDto) { + super({ externalRoomId, normalizedRoomId, externalEventId }); + this.redactsEvent = redactsEvent; + this.externalSenderId = externalSenderId; + } + + redactsEvent: string; + + externalSenderId: string; +} diff --git a/apps/meteor/app/federation-v2/server/application/sender/MessageSenders.ts b/apps/meteor/app/federation-v2/server/application/sender/MessageSenders.ts new file mode 100644 index 00000000000..8b59267e42f --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/sender/MessageSenders.ts @@ -0,0 +1,49 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import type { IFederationBridge } from '../../domain/IFederationBridge'; +import type { RocketChatFileAdapter } from '../../infrastructure/rocket-chat/adapters/File'; + +export interface IExternalMessageSender { + sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void>; +} + +class TextExternalMessageSender implements IExternalMessageSender { + constructor(private readonly bridge: IFederationBridge) {} + + public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> { + await this.bridge.sendMessage(externalRoomId, externalSenderId, message.msg); + } +} + +class FileExternalMessageSender implements IExternalMessageSender { + constructor(private readonly bridge: IFederationBridge, private readonly internalFileHelper: RocketChatFileAdapter) {} + + public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> { + const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id); + if (!file || !file.size || !file.type) { + return; + } + + const buffer = await this.internalFileHelper.getBufferFromFileRecord(file); + const metadata = await this.internalFileHelper.extractMetadataFromFile(file); + + await this.bridge.sendMessageFileToRoom(externalRoomId, externalSenderId, buffer, { + filename: file.name, + fileSize: file.size, + mimeType: file.type, + metadata: { + width: metadata?.width, + height: metadata?.height, + format: metadata?.format, + }, + }); + } +} + +export const getExternalMessageSender = ( + message: IMessage, + bridge: IFederationBridge, + internalFileHelper: RocketChatFileAdapter, +): IExternalMessageSender => { + return message.files ? new FileExternalMessageSender(bridge, internalFileHelper) : new TextExternalMessageSender(bridge); +}; diff --git a/apps/meteor/app/federation-v2/server/application/sender/MessageServiceSender.ts b/apps/meteor/app/federation-v2/server/application/sender/MessageServiceSender.ts new file mode 100644 index 00000000000..6abd358ff6a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/sender/MessageServiceSender.ts @@ -0,0 +1,98 @@ +import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import { isMessageFromMatrixFederation } from '@rocket.chat/core-typings'; + +import { FederatedUser } from '../../domain/FederatedUser'; +import type { IFederationBridge } from '../../domain/IFederationBridge'; +import { Federation } from '../../Federation'; +import type { RocketChatMessageAdapter } from '../../infrastructure/rocket-chat/adapters/Message'; +import type { RocketChatRoomAdapter } from '../../infrastructure/rocket-chat/adapters/Room'; +import type { RocketChatSettingsAdapter } from '../../infrastructure/rocket-chat/adapters/Settings'; +import type { RocketChatUserAdapter } from '../../infrastructure/rocket-chat/adapters/User'; + +export class FederationMessageServiceSender { + constructor( + protected internalRoomAdapter: RocketChatRoomAdapter, + protected internalUserAdapter: RocketChatUserAdapter, + protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected internalMessageAdapter: RocketChatMessageAdapter, + protected bridge: IFederationBridge, + ) {} + + public async sendExternalMessageReaction(internalMessage: IMessage, internalUser: IUser, reaction: string): Promise<void> { + if (!internalMessage || !internalUser || !internalUser._id || !internalMessage.rid) { + return; + } + const federatedSender = await this.internalUserAdapter.getFederatedUserByInternalId(internalUser._id); + if (!federatedSender) { + return; + } + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalMessage.rid); + if (!federatedRoom) { + return; + } + + if (!isMessageFromMatrixFederation(internalMessage)) { + return; + } + + const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( + this.bridge.extractHomeserverOrigin(federatedSender.getExternalId()), + this.internalSettingsAdapter.getHomeServerDomain(), + ); + if (!isUserFromTheSameHomeServer) { + return; + } + + const eventId = await this.bridge.sendMessageReaction( + federatedRoom.getExternalId(), + federatedSender.getExternalId(), + internalMessage.federation?.eventId as string, + reaction, + ); + federatedSender.getUsername() && + (await this.internalMessageAdapter.setExternalFederationEventOnMessage( + federatedSender.getUsername() as string, + internalMessage, + reaction, + eventId, + )); + } + + public async sendExternalMessageUnReaction(internalMessage: IMessage, internalUser: IUser, reaction: string): Promise<void> { + if (!internalMessage || !internalUser || !internalUser._id || !internalMessage.rid) { + return; + } + const federatedSender = await this.internalUserAdapter.getFederatedUserByInternalId(internalUser._id); + if (!federatedSender) { + return; + } + + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalMessage.rid); + if (!federatedRoom) { + return; + } + + if (!isMessageFromMatrixFederation(internalMessage)) { + return; + } + + const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( + this.bridge.extractHomeserverOrigin(federatedSender.getExternalId()), + this.internalSettingsAdapter.getHomeServerDomain(), + ); + if (!isUserFromTheSameHomeServer) { + return; + } + // TODO: leaked business logic, move this to the domain layer + const externalEventId = Object.keys(internalMessage.reactions?.[reaction].federationReactionEventIds || {}).find( + (key) => internalMessage.reactions?.[reaction].federationReactionEventIds?.[key] === internalUser.username, + ); + if (!externalEventId) { + return; + } + const normalizedEventId = Federation.unescapeExternalFederationEventId(externalEventId); + await this.bridge.redactEvent(federatedRoom.getExternalId(), federatedSender.getExternalId(), normalizedEventId); + await this.internalMessageAdapter.unsetExternalFederationEventOnMessage(externalEventId, internalMessage, reaction); + } +} diff --git a/apps/meteor/app/federation-v2/server/application/sender/RoomServiceSender.ts b/apps/meteor/app/federation-v2/server/application/sender/RoomServiceSender.ts index 721ee5f6518..990bc2fd52a 100644 --- a/apps/meteor/app/federation-v2/server/application/sender/RoomServiceSender.ts +++ b/apps/meteor/app/federation-v2/server/application/sender/RoomServiceSender.ts @@ -1,8 +1,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import { isDeletedMessage, isEditedMessage, isMessageFromMatrixFederation } from '@rocket.chat/core-typings'; import { DirectMessageFederatedRoom } from '../../domain/FederatedRoom'; import { FederatedUser } from '../../domain/FederatedUser'; import type { IFederationBridge } from '../../domain/IFederationBridge'; +import type { RocketChatFileAdapter } from '../../infrastructure/rocket-chat/adapters/File'; import type { RocketChatRoomAdapter } from '../../infrastructure/rocket-chat/adapters/Room'; import type { RocketChatSettingsAdapter } from '../../infrastructure/rocket-chat/adapters/Settings'; import type { RocketChatUserAdapter } from '../../infrastructure/rocket-chat/adapters/User'; @@ -13,12 +15,14 @@ import type { FederationCreateDMAndInviteUserDto, FederationRoomSendExternalMessageDto, } from '../input/RoomSenderDto'; +import { getExternalMessageSender } from './MessageSenders'; export class FederationRoomServiceSender extends FederationService { constructor( protected internalRoomAdapter: RocketChatRoomAdapter, protected internalUserAdapter: RocketChatUserAdapter, protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected internalFileAdapter: RocketChatFileAdapter, protected bridge: IFederationBridge, ) { super(bridge, internalUserAdapter, internalSettingsAdapter); @@ -140,7 +144,6 @@ export class FederationRoomServiceSender extends FederationService { public async sendExternalMessage(roomSendExternalMessageInput: FederationRoomSendExternalMessageDto): Promise<IMessage> { const { internalRoomId, internalSenderId, message } = roomSendExternalMessageInput; - const federatedSender = await this.internalUserAdapter.getFederatedUserByInternalId(internalSenderId); if (!federatedSender) { throw new Error(`Could not find user id for ${internalSenderId}`); @@ -150,9 +153,72 @@ export class FederationRoomServiceSender extends FederationService { if (!federatedRoom) { throw new Error(`Could not find room id for ${internalRoomId}`); } - - await this.bridge.sendMessage(federatedRoom.getExternalId(), federatedSender.getExternalId(), message.msg); + await getExternalMessageSender(message, this.bridge, this.internalFileAdapter).sendMessage( + federatedRoom.getExternalId(), + federatedSender.getExternalId(), + message, + ); return message; // this need to be here due to a limitation in the internal API that was expecting the return of the sendMessage function. } + + public async afterMessageDeleted(internalMessage: IMessage, internalRoomId: string): Promise<void> { + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + if (!federatedRoom) { + return; + } + + const federatedUser = internalMessage.u?._id && (await this.internalUserAdapter.getFederatedUserByInternalId(internalMessage.u._id)); + if (!federatedUser) { + return; + } + + if (!isMessageFromMatrixFederation(internalMessage) || isDeletedMessage(internalMessage)) { + return; + } + + const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( + this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()), + this.internalSettingsAdapter.getHomeServerDomain(), + ); + if (!isUserFromTheSameHomeServer) { + return; + } + + await this.bridge.redactEvent( + federatedRoom.getExternalId(), + federatedUser.getExternalId(), + internalMessage.federation?.eventId as string, + ); + } + + public async afterMessageUpdated(internalMessage: IMessage, internalRoomId: string, internalUserId: string): Promise<void> { + const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + if (!federatedRoom) { + return; + } + + const federatedUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalUserId); + if (!federatedUser) { + return; + } + if (!isMessageFromMatrixFederation(internalMessage) || !isEditedMessage(internalMessage) || internalMessage.u._id !== internalUserId) { + return; + } + + const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer( + this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()), + this.internalSettingsAdapter.getHomeServerDomain(), + ); + if (!isUserFromTheSameHomeServer) { + return; + } + + await this.bridge.updateMessage( + federatedRoom.getExternalId(), + federatedUser.getExternalId(), + internalMessage.federation?.eventId as string, + internalMessage.msg, + ); + } } diff --git a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts index 5a740830ddf..7a5f599850e 100644 --- a/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -1,5 +1,5 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { ObjectId } from 'mongodb'; // This should not be in the domain layer, but its a known "problem" import type { FederatedUser } from './FederatedUser'; @@ -95,6 +95,10 @@ export abstract class AbstractFederatedRoom { public shouldUpdateRoomTopic(aRoomTopic: string): boolean { return this.internalReference?.topic !== aRoomTopic && !this.isDirectMessage(); } + + public static shouldUpdateMessage(newMessageText: string, originalMessage: IMessage): boolean { + return originalMessage.msg !== newMessageText; + } } export class FederatedRoom extends AbstractFederatedRoom { diff --git a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts index da62c804cb4..0504133b33d 100644 --- a/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -23,4 +23,16 @@ export interface IFederationBridge { leaveRoom(externalRoomId: string, externalUserId: string): Promise<void>; kickUserFromRoom(externalRoomId: string, externalUserId: string, externalOwnerId: string): Promise<void>; logFederationStartupInfo(info?: string): void; + getReadStreamForFileFromUrl(externaUserId: string, fileUrl: string): Promise<ReadableStream>; + redactEvent(externalRoomId: string, externalUserId: string, externalEventId: string): Promise<void>; + updateMessage(externalRoomId: string, externalUserId: string, externalEventId: string, newMessageText: string): Promise<void>; + sendMessageReaction(externalRoomId: string, externalUserId: string, externalEventId: string, reaction: string): Promise<string>; + sendMessageFileToRoom( + externalRoomId: string, + externaSenderId: string, + content: Buffer, + fileDetails: { filename: string; fileSize: number; mimeType: string; metadata?: { width?: number; height?: number; format?: string } }, + ): Promise<void>; + uploadContent(externalSenderId: string, content: Buffer, options?: { name?: string; type?: string }): Promise<string | undefined>; + convertMatrixUrlToHttp(externalUserId: string, matrixUrl: string): string; } diff --git a/apps/meteor/app/federation-v2/server/index.ts b/apps/meteor/app/federation-v2/server/index.ts index c73d09a3167..da436c6c25c 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,3 +1,4 @@ +import type { FederationRoomServiceSender } from './application/sender/RoomServiceSender'; import { FederationFactory } from './infrastructure/Factory'; export const FEDERATION_PROCESSING_CONCURRENCY = 1; @@ -9,22 +10,37 @@ export const federationQueueInstance = FederationFactory.buildFederationQueue(); const federationBridge = FederationFactory.buildFederationBridge(rocketSettingsAdapter, federationQueueInstance); const rocketRoomAdapter = FederationFactory.buildRocketRoomAdapter(); const rocketUserAdapter = FederationFactory.buildRocketUserAdapter(); -const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); +export const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); +export const rocketFileAdapter = FederationFactory.buildRocketFileAdapter(); const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver( rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, + rocketFileAdapter, federationBridge, ); -const federationEventsHandler = FederationFactory.buildFederationEventHandler(federationRoomServiceReceiver, rocketSettingsAdapter); +const federationMessageServiceReceiver = FederationFactory.buildMessageServiceReceiver( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + federationBridge, +); -export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( +const federationEventsHandler = FederationFactory.buildFederationEventHandler( + federationRoomServiceReceiver, + federationMessageServiceReceiver, + rocketSettingsAdapter, +); + +export let federationRoomServiceSender = FederationFactory.buildRoomServiceSender( rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, + rocketFileAdapter, federationBridge, ); @@ -35,10 +51,25 @@ const federationRoomInternalHooksValidator = FederationFactory.buildRoomInternal federationBridge, ); -FederationFactory.setupListeners(federationRoomServiceSender, federationRoomInternalHooksValidator); +const federationMessageServiceSender = FederationFactory.buildMessageServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketMessageAdapter, + federationBridge, +); + let cancelSettingsObserver: () => void; export const runFederation = async (): Promise<void> => { + federationRoomServiceSender = FederationFactory.buildRoomServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketFileAdapter, + federationBridge, + ); + FederationFactory.setupListeners(federationRoomServiceSender, federationRoomInternalHooksValidator, federationMessageServiceSender); federationQueueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), FEDERATION_PROCESSING_CONCURRENCY); cancelSettingsObserver = rocketSettingsAdapter.onFederationEnabledStatusChanged( federationBridge.onFederationAvailabilityChanged.bind(federationBridge), @@ -51,7 +82,12 @@ export const runFederation = async (): Promise<void> => { require('./infrastructure/rocket-chat/slash-commands'); }; -export const stopFederation = async (): Promise<void> => { +const updateServiceSenderInstance = (federationRoomServiceSenderInstance: FederationRoomServiceSender) => { + federationRoomServiceSender = federationRoomServiceSenderInstance; +}; + +export const stopFederation = async (federationRoomServiceSenderInstance: FederationRoomServiceSender): Promise<void> => { + updateServiceSenderInstance(federationRoomServiceSenderInstance); FederationFactory.removeListeners(); await federationBridge.stop(); cancelSettingsObserver(); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts index d922f6b7aab..56034c01b2a 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -1,4 +1,4 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { FederationRoomServiceListener } from '../application/RoomServiceListener'; import { FederationRoomServiceSender } from '../application/sender/RoomServiceSender'; @@ -7,6 +7,7 @@ import { MatrixEventsHandler } from './matrix/handlers'; import type { MatrixBaseEventHandler } from './matrix/handlers/BaseEvent'; import { MatrixRoomCreatedHandler, + MatrixRoomEventRedactedHandler, MatrixRoomJoinRulesChangedHandler, MatrixRoomMembershipChangedHandler, MatrixRoomMessageSentHandler, @@ -22,6 +23,10 @@ import type { IFederationBridge } from '../domain/IFederationBridge'; import { FederationHooks } from './rocket-chat/hooks'; import { FederationRoomSenderConverter } from './rocket-chat/converters/RoomSender'; import { FederationRoomInternalHooksValidator } from '../application/sender/RoomInternalHooksValidator'; +import { RocketChatFileAdapter } from './rocket-chat/adapters/File'; +import { FederationMessageServiceListener } from '../application/MessageServiceListener'; +import { MatrixMessageReactedHandler } from './matrix/handlers/Message'; +import { FederationMessageServiceSender } from '../application/sender/MessageServiceSender'; export class FederationFactory { public static buildRocketSettingsAdapter(): RocketChatSettingsAdapter { @@ -40,6 +45,10 @@ export class FederationFactory { return new RocketChatMessageAdapter(); } + public static buildRocketFileAdapter(): RocketChatFileAdapter { + return new RocketChatFileAdapter(); + } + public static buildFederationQueue(): InMemoryQueue { return new InMemoryQueue(); } @@ -49,18 +58,47 @@ export class FederationFactory { rocketUserAdapter: RocketChatUserAdapter, rocketMessageAdapter: RocketChatMessageAdapter, rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketFileAdapter: RocketChatFileAdapter, bridge: IFederationBridge, ): FederationRoomServiceListener { - return new FederationRoomServiceListener(rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, bridge); + return new FederationRoomServiceListener( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + rocketFileAdapter, + bridge, + ); } public static buildRoomServiceSender( rocketRoomAdapter: RocketChatRoomAdapter, rocketUserAdapter: RocketChatUserAdapter, rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketFileAdapter: RocketChatFileAdapter, bridge: IFederationBridge, ): FederationRoomServiceSender { - return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, bridge); + return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketFileAdapter, bridge); + } + + public static buildMessageServiceSender( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, + bridge: IFederationBridge, + ): FederationMessageServiceSender { + return new FederationMessageServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketMessageAdapter, bridge); + } + + public static buildMessageServiceReceiver( + rocketRoomAdapter: RocketChatRoomAdapter, + rocketUserAdapter: RocketChatUserAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, + rocketSettingsAdapter: RocketChatSettingsAdapter, + bridge: IFederationBridge, + ): FederationMessageServiceListener { + return new FederationMessageServiceListener(rocketRoomAdapter, rocketUserAdapter, rocketMessageAdapter, rocketSettingsAdapter, bridge); } public static buildRoomInternalHooksValidator( @@ -86,13 +124,15 @@ export class FederationFactory { public static buildFederationEventHandler( roomServiceReceive: FederationRoomServiceListener, + messageServiceReceiver: FederationMessageServiceListener, rocketSettingsAdapter: RocketChatSettingsAdapter, ): MatrixEventsHandler { - return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive, rocketSettingsAdapter)); + return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive, messageServiceReceiver, rocketSettingsAdapter)); } public static getEventHandlers( roomServiceReceiver: FederationRoomServiceListener, + messageServiceReceiver: FederationMessageServiceListener, rocketSettingsAdapter: RocketChatSettingsAdapter, ): MatrixBaseEventHandler[] { return [ @@ -102,18 +142,21 @@ export class FederationFactory { new MatrixRoomJoinRulesChangedHandler(roomServiceReceiver), new MatrixRoomNameChangedHandler(roomServiceReceiver), new MatrixRoomTopicChangedHandler(roomServiceReceiver), + new MatrixRoomEventRedactedHandler(roomServiceReceiver), + new MatrixMessageReactedHandler(messageServiceReceiver), ]; } public static setupListeners( roomServiceSender: FederationRoomServiceSender, roomInternalHooksValidator: FederationRoomInternalHooksValidator, + messageServiceSender: FederationMessageServiceSender, ): void { - FederationFactory.setupActions(roomServiceSender); + FederationFactory.setupActions(roomServiceSender, messageServiceSender); FederationFactory.setupValidators(roomInternalHooksValidator); } - private static setupActions(roomServiceSender: FederationRoomServiceSender): void { + private static setupActions(roomServiceSender: FederationRoomServiceSender, messageServiceSender: FederationMessageServiceSender): void { FederationHooks.afterUserLeaveRoom((user: IUser, room: IRoom) => roomServiceSender.afterUserLeaveRoom(FederationRoomSenderConverter.toAfterUserLeaveRoom(user._id, room._id)), ); @@ -122,6 +165,16 @@ export class FederationFactory { FederationRoomSenderConverter.toOnUserRemovedFromRoom(user._id, room._id, userWhoRemoved._id), ), ); + FederationHooks.afterMessageReacted((message: IMessage, user: IUser, reaction: string) => + messageServiceSender.sendExternalMessageReaction(message, user, reaction), + ); + FederationHooks.afterMessageunReacted((message: IMessage, user: IUser, reaction: string) => + messageServiceSender.sendExternalMessageUnReaction(message, user, reaction), + ); + FederationHooks.afterMessageDeleted((message: IMessage, roomId: string) => roomServiceSender.afterMessageDeleted(message, roomId)); + FederationHooks.afterMessageUpdated((message: IMessage, roomId: string, userId: string) => + roomServiceSender.afterMessageUpdated(message, roomId, userId), + ); } private static setupValidators(roomInternalHooksValidator: FederationRoomInternalHooksValidator): void { diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts index fb9d6b614ec..c2da4da9b2a 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -1,8 +1,12 @@ import type { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge'; +import { fetch } from '../../../../../server/lib/http/fetch'; import type { IExternalUserProfileInformation, IFederationBridge } from '../../domain/IFederationBridge'; import { federationBridgeLogger } from '../rocket-chat/adapters/logger'; +import { convertEmojisRCFormatToMatrixFormat } from './converters/MessageReceiver'; import type { AbstractMatrixEvent } from './definitions/AbstractMatrixEvent'; +import { MatrixEnumRelatesToRelType, MatrixEnumSendMessageType } from './definitions/events/RoomMessageSent'; +import { MatrixEventType } from './definitions/MatrixEventType'; import { MatrixRoomType } from './definitions/MatrixRoomType'; import { MatrixRoomVisibility } from './definitions/MatrixRoomVisibility'; @@ -144,12 +148,25 @@ export class MatrixBridge implements IFederationBridge { public async sendMessage(externalRoomId: string, externaSenderId: string, text: string): Promise<void> { try { - await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, text); + await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, this.escapeEmojis(text)); } catch (e) { throw new Error('User is not part of the room.'); } } + private escapeEmojis(text: string): string { + return convertEmojisRCFormatToMatrixFormat(text); + } + + public async getReadStreamForFileFromUrl(externalUserId: string, fileUrl: string): Promise<ReadableStream> { + const response = await fetch(this.convertMatrixUrlToHttp(externalUserId, fileUrl)); + if (!response.body) { + throw new Error('Not able to download the file'); + } + + return response.body; + } + public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean { const userDomain = this.extractHomeserverOrigin(externalUserId); @@ -185,6 +202,114 @@ export class MatrixBridge implements IFederationBridge { await this.bridgeInstance.getIntent(externalOwnerId).kick(externalRoomId, externalUserId); } + public async redactEvent(externalRoomId: string, externalUserId: string, externalEventId: string): Promise<void> { + await this.bridgeInstance.getIntent(externalUserId).matrixClient.redactEvent(externalRoomId, externalEventId); + } + + public async sendMessageReaction( + externalRoomId: string, + externalUserId: string, + externalEventId: string, + reaction: string, + ): Promise<string> { + const eventId = await this.bridgeInstance + .getIntent(externalUserId) + .matrixClient.sendEvent(externalRoomId, MatrixEventType.MESSAGE_REACTED, { + 'm.relates_to': { + event_id: externalEventId, + key: convertEmojisRCFormatToMatrixFormat(reaction), + rel_type: 'm.annotation', + }, + }); + + return eventId; + } + + public async updateMessage( + externalRoomId: string, + externalUserId: string, + externalEventId: string, + newMessageText: string, + ): Promise<void> { + await this.bridgeInstance.getIntent(externalUserId).matrixClient.sendEvent(externalRoomId, MatrixEventType.ROOM_MESSAGE_SENT, { + 'body': ` * ${newMessageText}`, + 'm.new_content': { + body: newMessageText, + msgtype: MatrixEnumSendMessageType.TEXT, + }, + 'm.relates_to': { + rel_type: MatrixEnumRelatesToRelType.REPLACE, + event_id: externalEventId, + }, + 'msgtype': MatrixEnumSendMessageType.TEXT, + }); + } + + public async sendMessageFileToRoom( + externalRoomId: string, + externaSenderId: string, + content: Buffer, + fileDetails: { filename: string; fileSize: number; mimeType: string; metadata?: { width?: number; height?: number; format?: string } }, + ): Promise<void> { + try { + const mxcUrl = await this.bridgeInstance.getIntent(externaSenderId).uploadContent(content); + await this.bridgeInstance.getIntent(externaSenderId).sendMessage(externalRoomId, { + body: fileDetails.filename, + filename: fileDetails.filename, + info: { + size: fileDetails.fileSize, + mimetype: fileDetails.mimeType, + ...(fileDetails.metadata?.height && fileDetails.metadata?.width + ? { h: fileDetails.metadata?.height, w: fileDetails.metadata?.width } + : {}), + }, + msgtype: this.getMsgTypeBasedOnMimeType(fileDetails.mimeType), + url: mxcUrl, + }); + } catch (e: any) { + if (e.body?.includes('413') || e.body?.includes('M_TOO_LARGE')) { + throw new Error('File is too large'); + } + } + } + + private getMsgTypeBasedOnMimeType(mimeType: string): MatrixEnumSendMessageType { + const knownImageMimeTypes = ['image/jpeg', 'image/png', 'image/gif']; + const knownAudioMimeTypes = ['audio/mpeg', 'audio/ogg', 'audio/wav']; + const knownVideoMimeTypes = ['video/mp4', 'video/ogg', 'video/webm']; + + if (knownImageMimeTypes.includes(mimeType)) { + return MatrixEnumSendMessageType.IMAGE; + } + if (knownAudioMimeTypes.includes(mimeType)) { + return MatrixEnumSendMessageType.AUDIO; + } + if (knownVideoMimeTypes.includes(mimeType)) { + return MatrixEnumSendMessageType.VIDEO; + } + return MatrixEnumSendMessageType.FILE; + } + + public async uploadContent( + externalSenderId: string, + content: Buffer, + options?: { name?: string; type?: string }, + ): Promise<string | undefined> { + try { + const mxcUrl = await this.bridgeInstance.getIntent(externalSenderId).uploadContent(content, options); + + return mxcUrl; + } catch (e: any) { + if (e.body?.includes('413') || e.body?.includes('M_TOO_LARGE')) { + throw new Error('File is too large'); + } + } + } + + public convertMatrixUrlToHttp(externalUserId: string, matrixUrl: string): string { + return this.bridgeInstance.getIntent(externalUserId).matrixClient.mxcToHttp(matrixUrl); + } + protected async createInstance(): Promise<void> { federationBridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/MessageReceiver.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/MessageReceiver.ts new file mode 100644 index 00000000000..6fd409591a1 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/MessageReceiver.ts @@ -0,0 +1,21 @@ +import emojione from 'emojione'; + +import type { MatrixEventMessageReact } from '../definitions/events/MessageReacted'; +import { FederationMessageReactionEventDto } from '../../../application/input/MessageReceiverDto'; +import { convertExternalRoomIdToInternalRoomIdFormat } from './RoomReceiver'; + +const convertEmojisMatrixFormatToRCFormat = (emoji: string): string => emojione.toShort(emoji); +export const convertEmojisRCFormatToMatrixFormat = (emoji: string): string => emojione.shortnameToUnicode(emoji); + +export class MatrixMessageReceiverConverter { + public static toMessageReactionDto(externalEvent: MatrixEventMessageReact): FederationMessageReactionEventDto { + return new FederationMessageReactionEventDto({ + externalEventId: externalEvent.event_id, + externalRoomId: externalEvent.room_id, + normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), + externalSenderId: externalEvent.sender, + emoji: convertEmojisMatrixFormatToRCFormat(externalEvent.content['m.relates_to'].key), + externalReactedEventId: externalEvent.content['m.relates_to'].event_id, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts index dc4cdcbf2c8..284c806b610 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -6,7 +6,10 @@ import { FederationRoomChangeNameDto, FederationRoomChangeTopicDto, FederationRoomCreateInputDto, + FederationRoomEditExternalMessageDto, + FederationRoomReceiveExternalFileMessageDto, FederationRoomReceiveExternalMessageDto, + FederationRoomRedactEventDto, } from '../../../application/input/RoomReceiverDto'; import { EVENT_ORIGIN } from '../../../domain/IFederationBridge'; import type { MatrixEventRoomMembershipChanged } from '../definitions/events/RoomMembershipChanged'; @@ -19,6 +22,7 @@ import type { MatrixEventRoomJoinRulesChanged } from '../definitions/events/Room import type { MatrixEventRoomNameChanged } from '../definitions/events/RoomNameChanged'; import type { MatrixEventRoomTopicChanged } from '../definitions/events/RoomTopicChanged'; import type { AbstractMatrixEvent } from '../definitions/AbstractMatrixEvent'; +import type { MatrixEventRoomRedacted } from '../definitions/events/RoomEventRedacted'; export const removeExternalSpecificCharsFromExternalIdentifier = (matrixIdentifier = ''): string => { return matrixIdentifier.replace('@', '').replace('!', ''); @@ -38,7 +42,7 @@ export const extractServerNameFromExternalIdentifier = (identifier = ''): string return splitted.length > 1 ? splitted[1] : ''; }; -const convertExternalRoomIdToInternalRoomIdFormat = (matrixRoomId = ''): string => { +export const convertExternalRoomIdToInternalRoomIdFormat = (matrixRoomId = ''): string => { const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; const prefix = '!'; @@ -95,6 +99,7 @@ const tryToExtractAndConvertRoomTypeFromTheRoomState = ( export class MatrixRoomReceiverConverter { public static toRoomCreateDto(externalEvent: MatrixEventRoomCreated): FederationRoomCreateInputDto { return new FederationRoomCreateInputDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), ...tryToExtractExternalRoomNameFromTheRoomState(externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state), @@ -111,6 +116,7 @@ export class MatrixRoomReceiverConverter { homeServerDomain: string, ): FederationRoomChangeMembershipDto { return new FederationRoomChangeMembershipDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), ...tryToExtractExternalRoomNameFromTheRoomState(externalEvent.invite_room_state || externalEvent.unsigned?.invite_room_state), @@ -131,16 +137,55 @@ export class MatrixRoomReceiverConverter { public static toSendRoomMessageDto(externalEvent: MatrixEventRoomMessageSent): FederationRoomReceiveExternalMessageDto { return new FederationRoomReceiveExternalMessageDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), externalSenderId: externalEvent.sender, normalizedSenderId: removeExternalSpecificCharsFromExternalIdentifier(externalEvent.sender), - messageText: externalEvent.content?.body, + messageText: externalEvent.content.body, + }); + } + + public static toEditRoomMessageDto(externalEvent: MatrixEventRoomMessageSent): FederationRoomEditExternalMessageDto { + return new FederationRoomEditExternalMessageDto({ + externalEventId: externalEvent.event_id, + externalRoomId: externalEvent.room_id, + normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), + externalSenderId: externalEvent.sender, + normalizedSenderId: removeExternalSpecificCharsFromExternalIdentifier(externalEvent.sender), + newMessageText: externalEvent.content['m.new_content']?.body as string, + editsEvent: externalEvent.content['m.relates_to']?.event_id as string, + }); + } + + public static toSendRoomFileMessageDto(externalEvent: MatrixEventRoomMessageSent): FederationRoomReceiveExternalFileMessageDto { + if (!externalEvent.content.url) { + throw new Error('Missing url in the file message'); + } + if (!externalEvent.content.info?.mimetype) { + throw new Error('Missing mimetype in the file message info'); + } + if (!externalEvent.content.info?.size) { + throw new Error('Missing size in the file message info'); + } + + return new FederationRoomReceiveExternalFileMessageDto({ + externalEventId: externalEvent.event_id, + externalRoomId: externalEvent.room_id, + normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), + externalSenderId: externalEvent.sender, + normalizedSenderId: removeExternalSpecificCharsFromExternalIdentifier(externalEvent.sender), + filename: externalEvent.content.body, + url: externalEvent.content.url, + mimetype: externalEvent.content.info.mimetype, + size: externalEvent.content.info.size, + messageText: externalEvent.content.body, }); } public static toRoomChangeJoinRulesDto(externalEvent: MatrixEventRoomJoinRulesChanged): FederationRoomChangeJoinRulesDto { return new FederationRoomChangeJoinRulesDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), roomType: convertExternalJoinRuleToInternalRoomType(externalEvent.content?.join_rule), @@ -149,6 +194,7 @@ export class MatrixRoomReceiverConverter { public static toRoomChangeNameDto(externalEvent: MatrixEventRoomNameChanged): FederationRoomChangeNameDto { return new FederationRoomChangeNameDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), externalSenderId: externalEvent.sender, @@ -158,10 +204,21 @@ export class MatrixRoomReceiverConverter { public static toRoomChangeTopicDto(externalEvent: MatrixEventRoomTopicChanged): FederationRoomChangeTopicDto { return new FederationRoomChangeTopicDto({ + externalEventId: externalEvent.event_id, externalRoomId: externalEvent.room_id, normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), externalSenderId: externalEvent.sender, roomTopic: externalEvent.content?.topic, }); } + + public static toRoomRedactEventDto(externalEvent: MatrixEventRoomRedacted): FederationRoomRedactEventDto { + return new FederationRoomRedactEventDto({ + externalEventId: externalEvent.event_id, + externalRoomId: externalEvent.room_id, + normalizedRoomId: convertExternalRoomIdToInternalRoomIdFormat(externalEvent.room_id), + externalSenderId: externalEvent.sender, + redactsEvent: externalEvent.redacts as string, + }); + } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/AbstractMatrixEvent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/AbstractMatrixEvent.ts index 4f141e8cabb..d7a1082b972 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/AbstractMatrixEvent.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/AbstractMatrixEvent.ts @@ -17,6 +17,8 @@ export abstract class AbstractMatrixEvent { public user_id: string; + public redacts?: string; + public abstract content: IBaseEventContent; public abstract type: string; diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts index fd0fa4b2192..60e944f09ae 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts @@ -9,4 +9,6 @@ export enum MatrixEventType { // SET_ROOM_HISTORY_VISIBILITY = 'm.room.history_visibility', // SET_ROOM_GUEST_ACCESS = 'm.room.guest_access', ROOM_TOPIC_CHANGED = 'm.room.topic', + ROOM_EVENT_REDACTED = 'm.room.redaction', + MESSAGE_REACTED = 'm.reaction', } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/MessageReacted.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/MessageReacted.ts new file mode 100644 index 00000000000..9275ca423ce --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/MessageReacted.ts @@ -0,0 +1,17 @@ +import type { IBaseEventContent } from '../AbstractMatrixEvent'; +import { AbstractMatrixEvent } from '../AbstractMatrixEvent'; +import { MatrixEventType } from '../MatrixEventType'; + +export interface IMatrixEventContentMessageReacted extends IBaseEventContent { + 'm.relates_to': { + event_id: string; + key: string; + rel_type: string; + }; +} + +export class MatrixEventMessageReact extends AbstractMatrixEvent { + public content: IMatrixEventContentMessageReacted; + + public type = MatrixEventType.MESSAGE_REACTED; +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomEventRedacted.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomEventRedacted.ts new file mode 100644 index 00000000000..93dd83fb85f --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomEventRedacted.ts @@ -0,0 +1,11 @@ +import type { IBaseEventContent } from '../AbstractMatrixEvent'; +import { AbstractMatrixEvent } from '../AbstractMatrixEvent'; +import { MatrixEventType } from '../MatrixEventType'; + +export type IMatrixEventContentRoomRedacted = IBaseEventContent; + +export class MatrixEventRoomRedacted extends AbstractMatrixEvent { + public content: IMatrixEventContentRoomRedacted; + + public type = MatrixEventType.ROOM_EVENT_REDACTED; +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent.ts index 52ad3de06b9..16e7b2da2c6 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent.ts @@ -2,13 +2,47 @@ import type { IBaseEventContent } from '../AbstractMatrixEvent'; import { AbstractMatrixEvent } from '../AbstractMatrixEvent'; import { MatrixEventType } from '../MatrixEventType'; -export enum MatrixSendMessageType { - 'm.text', +type MatrixSendMessageType = 'm.text' | 'm.emote' | 'm.notice' | 'm.image' | 'm.file' | 'm.audio' | 'm.location' | 'm.video' | string; + +export enum MatrixEnumSendMessageType { + TEXT = 'm.text', + EMOTE = 'm.emote', + NOTICE = 'm.notice', + IMAGE = 'm.image', + FILE = 'm.file', + AUDIO = 'm.audio', + LOCATION = 'm.location', + VIDEO = 'm.video', +} + +interface IMatrixContentInfo { + mimetype: string; + size: number; + duration?: number; +} + +type MatrixRelatesToRelType = 'm.replace'; + +export enum MatrixEnumRelatesToRelType { + REPLACE = 'm.replace', } export interface IMatrixEventContentRoomMessageSent extends IBaseEventContent { - body: string; - msgtype: MatrixSendMessageType; + 'body': string; + 'msgtype': MatrixSendMessageType; + 'info'?: IMatrixContentInfo; + 'url'?: string; + 'format'?: string; + 'formatted_body'?: string; + 'geo_uri'?: string; + 'm.new_content'?: { + body: string; + msgtype: MatrixSendMessageType; + }; + 'm.relates_to'?: { + rel_type: MatrixRelatesToRelType; + event_id: string; + }; } export class MatrixEventRoomMessageSent extends AbstractMatrixEvent { diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Message.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Message.ts new file mode 100644 index 00000000000..8c429e093f5 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Message.ts @@ -0,0 +1,17 @@ +import type { FederationMessageServiceListener } from '../../../application/MessageServiceListener'; +import { MatrixMessageReceiverConverter } from '../converters/MessageReceiver'; +import type { MatrixEventMessageReact } from '../definitions/events/MessageReacted'; +import { MatrixEventType } from '../definitions/MatrixEventType'; +import { MatrixBaseEventHandler } from './BaseEvent'; + +export class MatrixMessageReactedHandler extends MatrixBaseEventHandler { + public eventType: string = MatrixEventType.MESSAGE_REACTED; + + constructor(private messageService: FederationMessageServiceListener) { + super(); + } + + public async handle(externalEvent: MatrixEventMessageReact): Promise<void> { + await this.messageService.onMessageReaction(MatrixMessageReceiverConverter.toMessageReactionDto(externalEvent)); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts index b9d596cefbd..739c7138460 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -6,9 +6,11 @@ import type { MatrixEventRoomCreated } from '../definitions/events/RoomCreated'; import type { MatrixEventRoomMembershipChanged } from '../definitions/events/RoomMembershipChanged'; import type { MatrixEventRoomJoinRulesChanged } from '../definitions/events/RoomJoinRulesChanged'; import type { MatrixEventRoomNameChanged } from '../definitions/events/RoomNameChanged'; -import type { MatrixEventRoomMessageSent } from '../definitions/events/RoomMessageSent'; +import type { IMatrixEventContentRoomMessageSent, MatrixEventRoomMessageSent } from '../definitions/events/RoomMessageSent'; +import { MatrixEnumRelatesToRelType, MatrixEnumSendMessageType } from '../definitions/events/RoomMessageSent'; import type { MatrixEventRoomTopicChanged } from '../definitions/events/RoomTopicChanged'; import { MatrixEventType } from '../definitions/MatrixEventType'; +import type { MatrixEventRoomRedacted } from '../definitions/events/RoomEventRedacted'; export class MatrixRoomCreatedHandler extends MatrixBaseEventHandler { public eventType: string = MatrixEventType.ROOM_CREATED; @@ -43,8 +45,39 @@ export class MatrixRoomMessageSentHandler extends MatrixBaseEventHandler { super(); } + private executeTextMessageHandler(eventContent: IMatrixEventContentRoomMessageSent, externalEvent: MatrixEventRoomMessageSent): any { + const isAnEditionEvent = + eventContent['m.new_content'] && + eventContent['m.relates_to'] && + eventContent['m.relates_to'].rel_type === MatrixEnumRelatesToRelType.REPLACE; + return isAnEditionEvent + ? this.roomService.onExternalMessageEditedReceived(MatrixRoomReceiverConverter.toEditRoomMessageDto(externalEvent)) + : this.roomService.onExternalMessageReceived(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + } + public async handle(externalEvent: MatrixEventRoomMessageSent): Promise<void> { - await this.roomService.onExternalMessageReceived(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + const handlers = { + [MatrixEnumSendMessageType.TEXT]: () => this.executeTextMessageHandler(externalEvent.content, externalEvent), + [MatrixEnumSendMessageType.AUDIO]: () => + this.roomService.onExternalFileMessageReceived(MatrixRoomReceiverConverter.toSendRoomFileMessageDto(externalEvent)), + [MatrixEnumSendMessageType.FILE]: () => + this.roomService.onExternalFileMessageReceived(MatrixRoomReceiverConverter.toSendRoomFileMessageDto(externalEvent)), + [MatrixEnumSendMessageType.IMAGE]: () => + this.roomService.onExternalFileMessageReceived(MatrixRoomReceiverConverter.toSendRoomFileMessageDto(externalEvent)), + [MatrixEnumSendMessageType.NOTICE]: () => + this.roomService.onExternalMessageReceived(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)), + [MatrixEnumSendMessageType.VIDEO]: () => + this.roomService.onExternalFileMessageReceived(MatrixRoomReceiverConverter.toSendRoomFileMessageDto(externalEvent)), + [MatrixEnumSendMessageType.EMOTE]: () => + this.roomService.onExternalMessageReceived(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)), + [MatrixEnumSendMessageType.LOCATION]: () => { + throw new Error('Location events are not supported yet'); + }, + }; + const defaultHandler = () => + this.roomService.onExternalMessageReceived(MatrixRoomReceiverConverter.toSendRoomMessageDto(externalEvent)); + + await (handlers[externalEvent.content.msgtype as MatrixEnumSendMessageType] || defaultHandler)(); } } @@ -83,3 +116,15 @@ export class MatrixRoomTopicChangedHandler extends MatrixBaseEventHandler { await this.roomService.onChangeRoomTopic(MatrixRoomReceiverConverter.toRoomChangeTopicDto(externalEvent)); } } + +export class MatrixRoomEventRedactedHandler extends MatrixBaseEventHandler { + public eventType: string = MatrixEventType.ROOM_EVENT_REDACTED; + + constructor(private roomService: FederationRoomServiceListener) { + super(); + } + + public async handle(externalEvent: MatrixEventRoomRedacted): Promise<void> { + await this.roomService.onRedactEvent(MatrixRoomReceiverConverter.toRoomRedactEventDto(externalEvent)); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts index d99a39edbf3..47469b74320 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/index.ts @@ -10,6 +10,10 @@ export class MatrixEventsHandler { if (!handler) { return console.log(`Could not find handler for ${event.type}`, event); } - return handler.handle(event); + try { + await handler.handle(event); + } catch (e: any) { + throw new Meteor.Error(e.message); + } } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/File.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/File.ts new file mode 100644 index 00000000000..a176c458be7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/File.ts @@ -0,0 +1,64 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage, IUpload, IUser } from '@rocket.chat/core-typings'; +import { Uploads } from '@rocket.chat/models'; + +import { FileUpload } from '../../../../../file-upload/server'; +import { parseFileIntoMessageAttachments } from '../../../../../file-upload/server/methods/sendFileMessage'; + +export class RocketChatFileAdapter { + public async uploadFile( + readableStream: ReadableStream, + internalRoomId: string, + internalUser: IUser, + fileRecord: Partial<IUpload>, + ): Promise<{ files: IMessage['files']; attachments: IMessage['attachments'] }> { + return new Promise<{ files: IMessage['files']; attachments: IMessage['attachments'] }>((resolve, reject) => { + const fileStore = FileUpload.getStore('Uploads'); + // this needs to be here due to a high coupling in the third party lib that rely on the logged in user + Meteor.runAsUser(internalUser._id, async () => { + const uploadedFile = fileStore.insertSync(fileRecord, readableStream); + try { + const { files, attachments } = await parseFileIntoMessageAttachments(uploadedFile, internalRoomId, internalUser); + + resolve({ files, attachments }); + } catch (error) { + reject(error); + } + }); + }); + } + + public async getBufferFromFileRecord(fileRecord: IUpload): Promise<Buffer> { + return new Promise((resolve, reject) => { + FileUpload.getBuffer(fileRecord, (err: Error, buffer: Buffer) => { + if (err) { + return reject(err); + } + resolve(buffer); + }); + }); + } + + public async getFileRecordById(fileId: string): Promise<IUpload | undefined | null> { + return Uploads.findOneById(fileId); + } + + public async extractMetadataFromFile(file: IUpload): Promise<{ height?: number; width?: number; format?: string }> { + if (file.type?.startsWith('image/')) { + const metadata = await FileUpload.extractMetadata(file); + + return { + format: metadata.format, + height: metadata.height, + width: metadata.width, + }; + } + if (file.type?.startsWith('video/')) { + return { + height: 200, + width: 250, + }; + } + return {}; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts index 9b5761b47e6..68550626f6b 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Message.ts @@ -1,9 +1,99 @@ -import { sendMessage } from '../../../../../lib/server'; +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { Messages } from '@rocket.chat/models'; + +import { deleteMessage, sendMessage, updateMessage } from '../../../../../lib/server'; +import { executeSetReaction } from '../../../../../reactions/server/setReaction'; import type { FederatedRoom } from '../../../domain/FederatedRoom'; import type { FederatedUser } from '../../../domain/FederatedUser'; +const DEFAULT_EMOJI_TO_REACT_WHEN_RECEIVED_EMOJI_DOES_NOT_EXIST = ':grey_question:'; + export class RocketChatMessageAdapter { - public async sendMessage(user: FederatedUser, room: FederatedRoom, messageText: string): Promise<void> { - sendMessage(user.getInternalReference(), { msg: messageText }, room.getInternalReference()); + public async sendMessage(user: FederatedUser, room: FederatedRoom, messageText: string, externalEventId: string): Promise<void> { + sendMessage(user.getInternalReference(), { federation: { eventId: externalEventId }, msg: messageText }, room.getInternalReference()); + } + + public async editMessage(user: FederatedUser, newMessageText: string, originalMessage: IMessage): Promise<void> { + const updatedMessage = Object.assign({}, originalMessage, { msg: newMessageText }); + updateMessage(updatedMessage, user.getInternalReference(), originalMessage); + } + + public async sendFileMessage( + user: FederatedUser, + room: FederatedRoom, + files: IMessage['files'], + attachments: IMessage['attachments'], + externalEventId: string, + ): Promise<void> { + Promise.resolve( + sendMessage( + user.getInternalReference(), + { + federation: { eventId: externalEventId }, + rid: room.getInternalId(), + ts: new Date(), + file: (files || [])[0], + files, + attachments, + }, + room.getInternalReference(), + ), + ); + } + + public async deleteMessage(message: IMessage, user: FederatedUser): Promise<void> { + deleteMessage(message, user.getInternalReference()); + } + + public async reactToMessage(user: FederatedUser, message: IMessage, reaction: string, externalEventId: string): Promise<void> { + // we need to run this as the user due to a high coupling in this function that relies on the logged in user + Meteor.runAsUser(user.getInternalId(), async () => { + try { + await executeSetReaction(reaction, message._id); + user.getUsername() && + (await Messages.setFederationReactionEventId(user.getUsername() as string, message._id, reaction, externalEventId)); + } catch (error: any) { + if (error?.message?.includes('Invalid emoji provided.')) { + await executeSetReaction(DEFAULT_EMOJI_TO_REACT_WHEN_RECEIVED_EMOJI_DOES_NOT_EXIST, message._id); + } + } + }); + } + + public async unreactToMessage(user: FederatedUser, message: IMessage, reaction: string, externalEventId: string): Promise<void> { + // we need to run this as the user due to a high coupling in this function that relies on the logged in user + Meteor.runAsUser(user.getInternalId(), async () => { + await executeSetReaction(reaction, message._id); + await Messages.unsetFederationReactionEventId(externalEventId, message._id, reaction); + }); + } + + public async findOneByFederationIdOnReactions(federationEventId: string, user: FederatedUser): Promise<IMessage | null | undefined> { + return ( + (user.getUsername() && Messages.findOneByFederationIdAndUsernameOnReactions(federationEventId, user.getUsername() as string)) || + undefined + ); + } + + public async getMessageByFederationId(federationEventId: string): Promise<IMessage | null> { + return Messages.findOneByFederationId(federationEventId); + } + + public async unsetExternalFederationEventOnMessage(externalEventId: string, message: IMessage, reaction: string): Promise<void> { + await Messages.unsetFederationReactionEventId(externalEventId, message._id, reaction); + } + + public async getMessageById(internalMessageId: string): Promise<IMessage | null> { + return Messages.findOneById(internalMessageId); + } + + public async setExternalFederationEventOnMessage( + username: string, + message: IMessage, + reaction: string, + externalEventId: string, + ): Promise<void> { + await Messages.setFederationReactionEventId(username, message._id, reaction, externalEventId); } } diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/MessageConverter.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/MessageConverter.ts new file mode 100644 index 00000000000..0559c471d4e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/MessageConverter.ts @@ -0,0 +1,7 @@ +export const escapeExternalFederationEventId = (externalEventId: string): string => { + return externalEventId.replace(/\$/g, '__sign__'); +}; + +export const unescapeExternalFederationEventId = (externalEventId: string): string => { + return externalEventId.replace(/__sign__/g, '$'); +}; diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts index 3ed04a3cffa..53224e5543b 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts @@ -72,6 +72,10 @@ export class RocketChatUserAdapter { return user; } + public async getInternalUserByUsername(username: string): Promise<IUser | undefined> { + return Users.findOneByUsername(username); + } + public async createFederatedUser(federatedUser: FederatedUser): Promise<void> { const existingLocalUser = federatedUser.getUsername() && (await Users.findOneByUsername(federatedUser.getUsername() as string)); if (existingLocalUser) { diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts index 407d4f760b8..fab94889a16 100644 --- a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts @@ -1,4 +1,5 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isMessageFromMatrixFederation, isRoomFederated, isEditedMessage } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../../../lib/callbacks'; @@ -7,7 +8,7 @@ export class FederationHooks { callbacks.add( 'afterLeaveRoom', (user: IUser, room: IRoom | undefined): void => { - if (!room?.federated) { + if (!room || !isRoomFederated(room)) { return; } Promise.await(callback(user, room)); @@ -21,7 +22,7 @@ export class FederationHooks { callbacks.add( 'afterRemoveFromRoom', (params: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom | undefined): void => { - if (!room?.federated) { + if (!room || !isRoomFederated(room)) { return; } Promise.await(callback(params.removedUser, room, params.userWhoRemoved)); @@ -64,6 +65,65 @@ export class FederationHooks { ); } + public static afterMessageReacted(callback: (message: IMessage, user: IUser, reaction: string) => Promise<void>): void { + callbacks.add( + 'afterSetReaction', + (message: IMessage, { user, reaction }: { user: IUser; reaction: string }): void => { + if (!message || !isMessageFromMatrixFederation(message)) { + return; + } + Promise.await(callback(message, user, reaction)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-message-reacted', + ); + } + + public static afterMessageunReacted(callback: (message: IMessage, user: IUser, reaction: string) => Promise<void>): void { + callbacks.add( + 'afterUnsetReaction', + (message: IMessage, { user, reaction, oldMessage }: any): void => { + if (!message || !isMessageFromMatrixFederation(message)) { + return; + } + Promise.await(callback(oldMessage, user, reaction)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-message-unreacted', + ); + } + + public static afterMessageDeleted(callback: (message: IMessage, roomId: IRoom['_id']) => Promise<void>): void { + callbacks.add( + 'afterDeleteMessage', + (message: IMessage, room: IRoom): void => { + if (!room || !isRoomFederated(room) || !isMessageFromMatrixFederation(message)) { + return; + } + Promise.await(callback(message, room._id)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-room-message-deleted', + ); + } + + public static afterMessageUpdated(callback: (message: IMessage, roomId: IRoom['_id'], userId: string) => Promise<void>): void { + callbacks.add( + 'afterSaveMessage', + (message: IMessage, room: IRoom): void => { + if (!room || !isRoomFederated(room) || !isMessageFromMatrixFederation(message)) { + return; + } + if (!isEditedMessage(message)) { + return; + } + Promise.await(callback(message, room._id, message.editedBy._id)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-room-message-updated', + ); + } + public static removeCEValidation(): void { callbacks.remove('federation.beforeAddUserAToRoom', 'federation-v2-can-add-federated-user-to-federated-room'); callbacks.remove('federation.beforeCreateDirectMessage', 'federation-v2-can-create-direct-message-from-ui-ce'); diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index dc99688c564..d0ae8333fe6 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -292,6 +292,10 @@ export const FileUpload = { return result; }, + async extractMetadata(file) { + return sharp(FileUpload.getBufferSync(file)).metadata(); + }, + createImageThumbnail(file) { if (!settings.get('Message_Attachments_Thumbnails_Enabled')) { return; diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 5f605323d04..ccba5bbc24d 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import type { MessageAttachment, FileAttachmentProps, IUser } from '@rocket.chat/core-typings'; +import type { MessageAttachment, FileAttachmentProps, IUser, IUpload, AtLeast } from '@rocket.chat/core-typings'; import { Rooms, Uploads } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; @@ -9,6 +9,112 @@ import { canAccessRoom } from '../../../authorization/server/functions/canAccess import { SystemLogger } from '../../../../server/lib/logger/system'; import { omit } from '../../../../lib/utils/omit'; +function validateFileRequiredFields(file: Partial<IUpload>): asserts file is AtLeast<IUpload, '_id' | 'name' | 'type' | 'size'> { + const requiredFields = ['_id', 'name', 'type', 'size']; + requiredFields.forEach((field) => { + if (!Object.keys(file).includes(field)) { + throw new Meteor.Error('error-invalid-file', 'Invalid file'); + } + }); +} + +export const parseFileIntoMessageAttachments = async ( + file: Partial<IUpload>, + roomId: string, + user: IUser, +): Promise<Record<string, any>> => { + validateFileRequiredFields(file); + + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); + + const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`); + + const attachments: MessageAttachment[] = []; + + const files = [ + { + _id: file._id, + name: file.name, + type: file.type, + }, + ]; + + if (/^image\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file?.description, + title_link: fileUrl, + title_link_download: true, + image_url: fileUrl, + image_type: file.type as string, + image_size: file.size, + }; + + if (file.identify?.size) { + attachment.image_dimensions = file.identify.size; + } + + try { + attachment.image_preview = await FileUpload.resizeImagePreview(file); + const thumbResult = await FileUpload.createImageThumbnail(file); + if (thumbResult) { + const { data: thumbBuffer, width, height } = thumbResult; + const thumbnail = FileUpload.uploadImageThumbnail(file, thumbBuffer, roomId, user._id); + const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name)}`); + attachment.image_url = thumbUrl; + attachment.image_type = thumbnail.type; + attachment.image_dimensions = { + width, + height, + }; + files.push({ + _id: thumbnail._id, + name: file.name, + type: thumbnail.type, + }); + } + } catch (e) { + SystemLogger.error(e); + } + attachments.push(attachment); + } else if (/^audio\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + audio_url: fileUrl, + audio_type: file.type as string, + audio_size: file.size, + }; + attachments.push(attachment); + } else if (/^video\/.+/.test(file.type as string)) { + const attachment: FileAttachmentProps = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + video_url: fileUrl, + video_type: file.type as string, + video_size: file.size as number, + }; + attachments.push(attachment); + } else { + const attachment = { + title: file.name, + type: 'file', + description: file.description, + title_link: fileUrl, + title_link_download: true, + }; + attachments.push(attachment); + } + return { files, attachments }; +}; + Meteor.methods({ async sendFileMessage(roomId, _store, file, msgData = {}) { const user = Meteor.user() as IUser | undefined; @@ -36,93 +142,7 @@ Meteor.methods({ tmid: Match.Optional(String), }); - await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); - - const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`); - - const attachments: MessageAttachment[] = []; - - const files = [ - { - _id: file._id, - name: file.name, - type: file.type, - }, - ]; - - if (/^image\/.+/.test(file.type)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - image_url: fileUrl, - image_type: file.type, - image_size: file.size, - }; - - if (file.identify?.size) { - attachment.image_dimensions = file.identify.size; - } - - try { - attachment.image_preview = await FileUpload.resizeImagePreview(file); - const thumbResult = await FileUpload.createImageThumbnail(file); - if (thumbResult) { - const { data: thumbBuffer, width, height } = thumbResult; - const thumbnail = FileUpload.uploadImageThumbnail(file, thumbBuffer, roomId, user._id); - const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name)}`); - attachment.image_url = thumbUrl; - attachment.image_type = thumbnail.type; - attachment.image_dimensions = { - width, - height, - }; - files.push({ - _id: thumbnail._id, - name: file.name, - type: thumbnail.type, - }); - } - } catch (e) { - SystemLogger.error(e); - } - attachments.push(attachment); - } else if (/^audio\/.+/.test(file.type)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - audio_url: fileUrl, - audio_type: file.type, - audio_size: file.size, - }; - attachments.push(attachment); - } else if (/^video\/.+/.test(file.type)) { - const attachment: FileAttachmentProps = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - video_url: fileUrl, - video_type: file.type, - video_size: file.size, - }; - attachments.push(attachment); - } else { - const attachment = { - title: file.name, - type: 'file', - description: file.description, - title_link: fileUrl, - title_link_download: true, - }; - attachments.push(attachment); - } + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); const msg = Meteor.call('sendMessage', { rid: roomId, diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index b80a513c59f..bdc3fb2c49f 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -49,7 +49,7 @@ export const deleteMessage = async function (message: IMessage, user: IUser): Pr }); } - const room = Rooms.findOneById(message.rid, { fields: { lastMessage: 1, prid: 1, mid: 1 } }); + const room = Rooms.findOneById(message.rid, { fields: { lastMessage: 1, prid: 1, mid: 1, federated: 1 } }); callbacks.run('afterDeleteMessage', deletedMsg, room); // update last message diff --git a/apps/meteor/app/reactions/client/init.js b/apps/meteor/app/reactions/client/init.js index b20a36dee39..b76009a403c 100644 --- a/apps/meteor/app/reactions/client/init.js +++ b/apps/meteor/app/reactions/client/init.js @@ -68,7 +68,7 @@ Meteor.startup(function () { id: 'reaction-message', icon: 'add-reaction', label: 'Add_Reaction', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], action(event, props) { event.stopPropagation(); const { message = messageArgs(this).msg } = props; diff --git a/apps/meteor/app/reactions/server/setReaction.js b/apps/meteor/app/reactions/server/setReaction.js index 9cfbd264df8..310d0aefc8b 100644 --- a/apps/meteor/app/reactions/server/setReaction.js +++ b/apps/meteor/app/reactions/server/setReaction.js @@ -57,6 +57,7 @@ async function setReaction(room, user, message, reaction, shouldReact) { let isReacted; if (userAlreadyReacted) { + const oldMessage = JSON.parse(JSON.stringify(message)); removeUserReaction(message, reaction, user.username); if (_.isEmpty(message.reactions)) { delete message.reactions; @@ -71,7 +72,7 @@ async function setReaction(room, user, message, reaction, shouldReact) { } } callbacks.run('unsetReaction', message._id, reaction); - callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact }); + callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact, oldMessage }); isReacted = false; } else { diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index c775af9fa86..92c801d2be8 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -146,10 +146,6 @@ } } - &__federation_icon { - width: 20px; - } - &__action-menu { position: relative; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index c1e944faaed..56e1bc9cd0b 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -17,13 +17,9 @@ {{/if}} <label class="rc-message-box__container"> - {{#unless isFederatedRoom}} - <span class="rc-message-box__icon emoji-picker-icon {{#unless isEmojiEnabled}}emoji-picker-icon--disabled{{/unless}} js-emoji-picker" aria-haspopup="true"> - {{> icon block="rc-input__icon-svg" icon="emoji"}} - </span> - {{else}} - <div class="rc-message-box__federation_icon"></div> - {{/unless}} + <span class="rc-message-box__icon emoji-picker-icon {{#unless isEmojiEnabled}}emoji-picker-icon--disabled{{/unless}} js-emoji-picker" aria-haspopup="true"> + {{> icon block="rc-input__icon-svg" icon="emoji"}} + </span> <textarea aria-label="{{_ 'Message'}}" name="msg" maxlength="{{maxMessageLength}}" placeholder="{{_ 'Message'}}" rows="1" class="rc-message-box__textarea js-input-message"></textarea> <div class="js-input-message-shadow"></div> @@ -35,22 +31,20 @@ {{# if customAction }} {{> Template.dynamic template=customAction.template data=customAction.data }} {{ else }} - {{#unless isFederatedRoom}} - {{#if canSend}} - {{> messageBoxAudioMessage rid=rid tmid=tmid}} - <span class="rc-message-box__action-menu js-action-menu" data-desktop aria-haspopup="true" data-qa-id="menu-more-actions"> - {{#if actions}} - <span class="rc-message-box__icon"> - {{> icon block="rc-input__icon-svg" icon="plus"}} - </span> - {{/if}} - </span> - {{else}} - <button class="js-join rc-button rc-button--primary rc-message-box__join-button"> - {{_ "join"}} - </button> - {{/if}} - {{/unless}} + {{#if canSend}} + {{> messageBoxAudioMessage rid=rid tmid=tmid}} + <span class="rc-message-box__action-menu js-action-menu" data-desktop aria-haspopup="true" data-qa-id="menu-more-actions"> + {{#if actions}} + <span class="rc-message-box__icon"> + {{> icon block="rc-input__icon-svg" icon="plus"}} + </span> + {{/if}} + </span> + {{else}} + <button class="js-join rc-button rc-button--primary rc-message-box__join-button"> + {{_ "join"}} + </button> + {{/if}} {{/if}} {{/if}} diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 9620030fda2..623eaa2b927 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -279,6 +279,7 @@ Template.messageBox.helpers({ }, actions() { const actionGroups = messageBox.actions.get(); + return Object.values(actionGroups).reduce((actions, actionGroup) => [...actions, ...actionGroup], []); }, formattingButtons() { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts index 3b808b75222..e6297e8c734 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { VRecDialog } from '../../../ui-vrecord/client'; import { messageBox } from '../../../ui-utils/client'; @@ -8,6 +9,7 @@ import { fileUpload } from '../../../ui'; import { settings } from '../../../settings/client'; import { imperativeModal } from '../../../../client/lib/imperativeModal'; import ShareLocationModal from '../../../../client/views/room/ShareLocation/ShareLocationModal'; +import { Rooms } from '../../../models/client'; messageBox.actions.add('Create_new', 'Video_message', { id: 'video-message', @@ -69,7 +71,13 @@ const canGetGeolocation = new ReactiveVar(false); messageBox.actions.add('Share', 'My_location', { id: 'share-location', icon: 'map-pin', - condition: () => canGetGeolocation.get(), + condition: () => { + const room = Rooms.findOne(Session.get('openedRoom')); + if (!room) { + return false; + } + return canGetGeolocation.get() && !isRoomFederated(room); + }, async action({ rid, tmid }) { imperativeModal.open({ component: ShareLocationModal, props: { rid, tmid, onClose: imperativeModal.close } }); }, diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 4b68a6fbdb3..ea1d8df72c2 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -32,7 +32,7 @@ export const addMessageToList = (messagesList: IMessage[], message: IMessage): I }; type MessageActionGroup = 'message' | 'menu'; -type MessageActionContext = 'message' | 'threads' | 'message-mobile' | 'pinned' | 'direct' | 'starred' | 'mentions' | 'federated'; +export type MessageActionContext = 'message' | 'threads' | 'message-mobile' | 'pinned' | 'direct' | 'starred' | 'mentions' | 'federated'; type MessageActionConditionProps = { message: IMessage; diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index e2800d80bd5..c2e736e7989 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Session } from 'meteor/session'; import type { IMessage } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; @@ -150,7 +151,7 @@ Meteor.startup(async function () { id: 'edit-message', icon: 'edit', label: 'Edit', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], action(_, props) { const { message = messageArgs(this).msg } = props; const element = document.getElementById(message.tmid ? `thread-${message._id}` : message._id); @@ -159,10 +160,13 @@ Meteor.startup(async function () { } getChatMessagesFrom(message).edit(element); }, - condition({ message, subscription, settings }) { + condition({ message, subscription, settings, room }) { if (subscription == null) { return false; } + if (isRoomFederated(room)) { + return message.u._id === Meteor.userId(); + } const hasPermission = hasAtLeastOnePermission('edit-message', message.rid); const isEditAllowed = settings.Message_AllowEditing; const editOwn = message.u && message.u._id === Meteor.userId(); @@ -191,7 +195,7 @@ Meteor.startup(async function () { id: 'delete-message', icon: 'trash', label: 'Delete', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], color: 'alert', action(_, props) { const { message = messageArgs(this).msg } = props; @@ -201,6 +205,9 @@ Meteor.startup(async function () { if (!subscription) { return false; } + if (isRoomFederated(room)) { + return message.u._id === Meteor.userId(); + } const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); if (isLivechatRoom) { return false; diff --git a/apps/meteor/app/ui/client/lib/fileUpload.ts b/apps/meteor/app/ui/client/lib/fileUpload.ts index fda1586be4a..d5c5a22fbe5 100644 --- a/apps/meteor/app/ui/client/lib/fileUpload.ts +++ b/apps/meteor/app/ui/client/lib/fileUpload.ts @@ -2,6 +2,7 @@ import { Tracker } from 'meteor/tracker'; import { Session } from 'meteor/session'; import { Random } from 'meteor/random'; import { Meteor } from 'meteor/meteor'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { settings } from '../../../settings/client'; import { UserAction, USER_ACTIVITIES } from './UserAction'; @@ -11,6 +12,7 @@ import FileUploadModal from '../../../../client/views/room/modals/FileUploadModa import { prependReplies } from '../../../../client/lib/utils/prependReplies'; import { chatMessages } from './ChatMessages'; import { getErrorMessage } from '../../../../client/lib/errorHandling'; +import { Rooms } from '../../../models/client'; export type Uploading = { id: string; @@ -188,6 +190,7 @@ export const fileUpload = async ( const key = ['messagebox', rid, tmid].filter(Boolean).join('_'); const messageBoxText = Meteor._localStorage.getItem(key) || ''; + const room = Rooms.findOne({ _id: rid }); const uploadNextFile = (): void => { const file = files.pop(); @@ -201,6 +204,7 @@ export const fileUpload = async ( file: file.file, fileName: file.name, fileDescription: messageBoxText, + showDescription: room && !isRoomFederated(room), onClose: (): void => { imperativeModal.close(); uploadNextFile(); diff --git a/apps/meteor/app/ui/client/views/app/lib/dropzone.ts b/apps/meteor/app/ui/client/views/app/lib/dropzone.ts index 6aaffe3e71a..4a11442e224 100644 --- a/apps/meteor/app/ui/client/views/app/lib/dropzone.ts +++ b/apps/meteor/app/ui/client/views/app/lib/dropzone.ts @@ -1,9 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; import moment from 'moment'; import _ from 'underscore'; -import { Users, Rooms } from '../../../../../models/client'; +import { Users } from '../../../../../models/client'; import { roomCoordinator } from '../../../../../../client/lib/rooms/roomCoordinator'; import { settings } from '../../../../../settings/client'; import { RoomManager } from '../../../../../ui-utils/client'; @@ -56,11 +55,6 @@ export const dropzoneHelpers = { }, dragAndDropLabel(this: { _id: IRoom['_id']; rid: IRoom['_id'] }): string { - const room = Rooms.findOne({ _id: this.rid }); - if (isRoomFederated(room)) { - return 'FileUpload_Disabled_for_federation'; - } - if (!userCanDrop(this._id)) { return 'error-not-allowed'; } @@ -125,12 +119,10 @@ export const dropzoneEvents = { ) { event.currentTarget.parentNode.classList.remove('over'); - const room = Rooms.findOne({ _id: this.rid }); - event.stopPropagation(); event.preventDefault(); - if (isRoomFederated(room) || !userCanDrop(this._id) || !settings.get('FileUpload_Enabled')) { + if (!userCanDrop(this._id) || !settings.get('FileUpload_Enabled')) { return false; } diff --git a/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts index a48aac319aa..ab1132318c1 100644 --- a/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts @@ -1,4 +1,4 @@ -import { IRoom, isRoomFederated } from '@rocket.chat/core-typings'; +import { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactNode, useCallback, useMemo } from 'react'; @@ -27,7 +27,6 @@ export const useFileUploadDropTarget = ( const t = useTranslation(); const fileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; - const roomFederated = isRoomFederated(room); const fileUploadAllowedForUser = useReactiveValue( useCallback( () => !roomCoordinator.readOnly(room._id, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })), @@ -61,14 +60,6 @@ export const useFileUploadDropTarget = ( } as const; } - if (roomFederated) { - return { - enabled: false, - reason: t('FileUpload_Disabled_for_federation'), - ...overlayProps, - } as const; - } - if (!fileUploadAllowedForUser) { return { enabled: false, @@ -82,7 +73,7 @@ export const useFileUploadDropTarget = ( onFileDrop, ...overlayProps, } as const; - }, [fileUploadAllowedForUser, fileUploadEnabled, onFileDrop, overlayProps, roomFederated, t]); + }, [fileUploadAllowedForUser, fileUploadEnabled, onFileDrop, overlayProps, t]); return [triggerProps, allOverlayProps] as const; }; diff --git a/apps/meteor/client/views/room/lib/Toolbox/defaultActions.ts b/apps/meteor/client/views/room/lib/Toolbox/defaultActions.ts index 4f1eb6d8420..6cc26abff31 100644 --- a/apps/meteor/client/views/room/lib/Toolbox/defaultActions.ts +++ b/apps/meteor/client/views/room/lib/Toolbox/defaultActions.ts @@ -1,4 +1,3 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; import { usePermission } from '@rocket.chat/ui-contexts'; import { useMemo, lazy } from 'react'; @@ -58,21 +57,13 @@ addAction('members-list', ({ room }) => { ); }); -addAction('uploaded-files-list', ({ room }) => { - const federated = isRoomFederated(room); - - return { - groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], - id: 'uploaded-files-list', - title: 'Files', - icon: 'clip', - ...(federated && { - 'disabled': true, - 'data-tooltip': 'Files_unavailable_for_federation', - }), - template: lazy(() => import('../../contextualBar/RoomFiles')), - order: 7, - }; +addAction('uploaded-files-list', { + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + id: 'uploaded-files-list', + title: 'Files', + icon: 'clip', + template: lazy(() => import('../../contextualBar/RoomFiles')), + order: 7, }); addAction('keyboard-shortcut-list', { diff --git a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx index 000065367a7..d5c6724840f 100644 --- a/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx +++ b/apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx @@ -12,6 +12,7 @@ type FileUploadModalProps = { fileName: string; fileDescription?: string; invalidContentType: boolean; + showDescription?: boolean; }; const FileUploadModal = ({ @@ -21,6 +22,7 @@ const FileUploadModal = ({ fileDescription, onSubmit, invalidContentType, + showDescription = true, }: FileUploadModalProps): ReactElement => { const [name, setName] = useState<string>(fileName); const [description, setDescription] = useState<string>(fileDescription || ''); @@ -86,12 +88,14 @@ const FileUploadModal = ({ </Field.Row> {!name && <Field.Error>{t('error-the-field-is-required', { field: t('Name') })}</Field.Error>} </Field> - <Field> - <Field.Label>{t('Upload_file_description')}</Field.Label> - <Field.Row> - <TextInput value={description} onChange={handleDescription} placeholder={t('Description')} ref={ref} /> - </Field.Row> - </Field> + {showDescription && ( + <Field> + <Field.Label>{t('Upload_file_description')}</Field.Label> + <Field.Row> + <TextInput value={description} onChange={handleDescription} placeholder={t('Description')} ref={ref} /> + </Field.Row> + </Field> + )} </FieldGroup> </Modal.Content> <Modal.Footer> diff --git a/apps/meteor/ee/app/federation-v2/server/application/input/RoomReceiverDto.ts b/apps/meteor/ee/app/federation-v2/server/application/input/RoomReceiverDto.ts deleted file mode 100644 index 66aeafd9139..00000000000 --- a/apps/meteor/ee/app/federation-v2/server/application/input/RoomReceiverDto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; - -import type { IFederationReceiverBaseRoomInputDto } from '../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; -import { FederationBaseRoomInputDto } from '../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; - -export interface IFederationCreateInputDto extends IFederationReceiverBaseRoomInputDto { - roomType: RoomType; -} - -export interface IFederationRoomNameChangeInputDto extends IFederationReceiverBaseRoomInputDto { - normalizedRoomName: string; -} - -export interface IFederationRoomChangeTopicInputDto extends IFederationReceiverBaseRoomInputDto { - roomTopic: string; -} - -export class FederationRoomChangeJoinRulesDto extends FederationBaseRoomInputDto { - constructor({ roomType, externalRoomId, normalizedRoomId }: IFederationCreateInputDto) { - super({ externalRoomId, normalizedRoomId }); - this.roomType = roomType; - } - - roomType: RoomType; -} - -export class FederationRoomChangeNameDto extends FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId, normalizedRoomName }: IFederationRoomNameChangeInputDto) { - super({ externalRoomId, normalizedRoomId }); - this.normalizedRoomName = normalizedRoomName; - } - - normalizedRoomName: string; -} - -export class FederationRoomChangeTopicDto extends FederationBaseRoomInputDto { - constructor({ externalRoomId, normalizedRoomId, roomTopic }: IFederationRoomChangeTopicInputDto) { - super({ externalRoomId, normalizedRoomId }); - this.roomTopic = roomTopic; - } - - roomTopic: string; -} diff --git a/apps/meteor/ee/app/federation-v2/server/application/sender/RoomServiceSender.ts b/apps/meteor/ee/app/federation-v2/server/application/sender/RoomServiceSender.ts deleted file mode 100644 index 10f5643e15c..00000000000 --- a/apps/meteor/ee/app/federation-v2/server/application/sender/RoomServiceSender.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { FederationService } from '../../../../../../app/federation-v2/server/application/AbstractFederationService'; -import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; -import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; -import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; -import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; -import type { FederationCreateDirectMessageDto } from '../input/RoomSenderDto'; - -export class FederationRoomServiceSenderEE extends FederationService { - constructor( - protected internalRoomAdapter: RocketChatRoomAdapterEE, - protected internalUserAdapter: RocketChatUserAdapterEE, - protected internalSettingsAdapter: RocketChatSettingsAdapter, - protected bridge: IFederationBridgeEE, - ) { - super(bridge, internalUserAdapter, internalSettingsAdapter); - } - - public async createLocalDirectMessageRoom(dmRoomCreateInput: FederationCreateDirectMessageDto): Promise<void> { - const { internalInviterId, invitees } = dmRoomCreateInput; - - await this.internalRoomAdapter.createLocalDirectMessageRoom(invitees, internalInviterId); - } -} diff --git a/apps/meteor/ee/app/federation-v2/server/application/sender/DMRoomInternalHooksServiceSender.ts b/apps/meteor/ee/app/federation-v2/server/application/sender/room/DMRoomInternalHooksServiceSender.ts similarity index 90% rename from apps/meteor/ee/app/federation-v2/server/application/sender/DMRoomInternalHooksServiceSender.ts rename to apps/meteor/ee/app/federation-v2/server/application/sender/room/DMRoomInternalHooksServiceSender.ts index 0f925b00f02..107f51f4fb4 100644 --- a/apps/meteor/ee/app/federation-v2/server/application/sender/DMRoomInternalHooksServiceSender.ts +++ b/apps/meteor/ee/app/federation-v2/server/application/sender/room/DMRoomInternalHooksServiceSender.ts @@ -1,14 +1,14 @@ -import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; -import { FederatedUserEE } from '../../domain/FederatedUser'; -import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; -import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; -import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; +import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; +import { FederatedUserEE } from '../../../domain/FederatedUser'; +import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; +import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; +import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; import type { FederationBeforeDirectMessageRoomCreationDto, FederationOnDirectMessageRoomCreationDto, FederationRoomInviteUserDto, -} from '../input/RoomSenderDto'; -import { FederationServiceEE } from './AbstractFederationService'; +} from '../../input/RoomSenderDto'; +import { FederationServiceEE } from '../AbstractFederationService'; export class FederationDMRoomInternalHooksServiceSender extends FederationServiceEE { constructor( diff --git a/apps/meteor/ee/app/federation-v2/server/application/sender/RoomInternalHooksServiceSender.ts b/apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomInternalHooksServiceSender.ts similarity index 90% rename from apps/meteor/ee/app/federation-v2/server/application/sender/RoomInternalHooksServiceSender.ts rename to apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomInternalHooksServiceSender.ts index 605bc10bfe4..5080198f9bf 100644 --- a/apps/meteor/ee/app/federation-v2/server/application/sender/RoomInternalHooksServiceSender.ts +++ b/apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomInternalHooksServiceSender.ts @@ -1,23 +1,25 @@ -import type { RocketChatSettingsAdapter } from '../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; -import { FederatedRoomEE } from '../../domain/FederatedRoom'; -import { FederatedUserEE } from '../../domain/FederatedUser'; -import type { IFederationBridgeEE } from '../../domain/IFederationBridge'; -import type { RocketChatRoomAdapterEE } from '../../infrastructure/rocket-chat/adapters/Room'; -import type { RocketChatUserAdapterEE } from '../../infrastructure/rocket-chat/adapters/User'; +import type { RocketChatMessageAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Message'; +import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; +import { FederatedRoomEE } from '../../../domain/FederatedRoom'; +import { FederatedUserEE } from '../../../domain/FederatedUser'; +import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; +import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; +import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; import type { FederationBeforeAddUserToARoomDto, FederationOnRoomCreationDto, FederationOnUsersAddedToARoomDto, FederationRoomInviteUserDto, FederationSetupRoomDto, -} from '../input/RoomSenderDto'; -import { FederationServiceEE } from './AbstractFederationService'; +} from '../../input/RoomSenderDto'; +import { FederationServiceEE } from '../AbstractFederationService'; export class FederationRoomInternalHooksServiceSender extends FederationServiceEE { constructor( protected internalRoomAdapter: RocketChatRoomAdapterEE, protected internalUserAdapter: RocketChatUserAdapterEE, protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected internalMessageAdapter: RocketChatMessageAdapter, protected bridge: IFederationBridgeEE, ) { super(bridge, internalUserAdapter, internalSettingsAdapter); diff --git a/apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomServiceSender.ts b/apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomServiceSender.ts new file mode 100644 index 00000000000..f17dbd9a71f --- /dev/null +++ b/apps/meteor/ee/app/federation-v2/server/application/sender/room/RoomServiceSender.ts @@ -0,0 +1,25 @@ +import { FederationRoomServiceSender } from '../../../../../../../app/federation-v2/server/application/sender/RoomServiceSender'; +import type { RocketChatFileAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/File'; +import type { RocketChatSettingsAdapter } from '../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; +import type { IFederationBridgeEE } from '../../../domain/IFederationBridge'; +import type { RocketChatRoomAdapterEE } from '../../../infrastructure/rocket-chat/adapters/Room'; +import type { RocketChatUserAdapterEE } from '../../../infrastructure/rocket-chat/adapters/User'; +import type { FederationCreateDirectMessageDto } from '../../input/RoomSenderDto'; + +export class FederationRoomServiceSenderEE extends FederationRoomServiceSender { + constructor( + protected internalRoomAdapter: RocketChatRoomAdapterEE, + protected internalUserAdapter: RocketChatUserAdapterEE, + protected internalSettingsAdapter: RocketChatSettingsAdapter, + protected internalFileAdapter: RocketChatFileAdapter, + protected bridge: IFederationBridgeEE, + ) { + super(internalRoomAdapter, internalUserAdapter, internalSettingsAdapter, internalFileAdapter, bridge); + } + + public async createLocalDirectMessageRoom(dmRoomCreateInput: FederationCreateDirectMessageDto): Promise<void> { + const { internalInviterId, invitees } = dmRoomCreateInput; + + await this.internalRoomAdapter.createLocalDirectMessageRoom(invitees, internalInviterId); + } +} diff --git a/apps/meteor/ee/app/federation-v2/server/index.ts b/apps/meteor/ee/app/federation-v2/server/index.ts index 172c31f578d..3df70d8004b 100644 --- a/apps/meteor/ee/app/federation-v2/server/index.ts +++ b/apps/meteor/ee/app/federation-v2/server/index.ts @@ -1,4 +1,11 @@ -import { runFederation, stopFederation, rocketSettingsAdapter, federationQueueInstance } from '../../../../app/federation-v2/server'; +import { + runFederation, + stopFederation, + rocketSettingsAdapter, + federationQueueInstance, + rocketMessageAdapter, + rocketFileAdapter, +} from '../../../../app/federation-v2/server'; import { onToggledFeature } from '../../license/server/license'; import { FederationFactoryEE } from './infrastructure/Factory'; @@ -10,6 +17,7 @@ export const federationRoomServiceSenderEE = FederationFactoryEE.buildRoomServic rocketRoomAdapterEE, rocketUserAdapterEE, rocketSettingsAdapter, + rocketFileAdapter, federationBridgeEE, ); @@ -17,6 +25,7 @@ export const federationRoomInternalHooksServiceSenderEE = FederationFactoryEE.bu rocketRoomAdapterEE, rocketUserAdapterEE, rocketSettingsAdapter, + rocketMessageAdapter, federationBridgeEE, ); @@ -36,7 +45,7 @@ let cancelSettingsObserverEE: () => void; onToggledFeature('federation', { up: async () => { - await stopFederation(); + await stopFederation(federationRoomServiceSenderEE); cancelSettingsObserverEE = rocketSettingsAdapter.onFederationEnabledStatusChanged( federationBridgeEE.onFederationAvailabilityChanged.bind(federationBridgeEE), ); diff --git a/apps/meteor/ee/app/federation-v2/server/infrastructure/Factory.ts b/apps/meteor/ee/app/federation-v2/server/infrastructure/Factory.ts index b966a0380fc..10b58c91d03 100644 --- a/apps/meteor/ee/app/federation-v2/server/infrastructure/Factory.ts +++ b/apps/meteor/ee/app/federation-v2/server/infrastructure/Factory.ts @@ -2,9 +2,9 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import type { InMemoryQueue } from '../../../../../app/federation-v2/server/infrastructure/queue/InMemoryQueue'; import type { RocketChatSettingsAdapter } from '../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings'; -import { FederationDMRoomInternalHooksServiceSender } from '../application/sender/DMRoomInternalHooksServiceSender'; -import { FederationRoomInternalHooksServiceSender } from '../application/sender/RoomInternalHooksServiceSender'; -import { FederationRoomServiceSenderEE } from '../application/sender/RoomServiceSender'; +import { FederationDMRoomInternalHooksServiceSender } from '../application/sender/room/DMRoomInternalHooksServiceSender'; +import { FederationRoomInternalHooksServiceSender } from '../application/sender/room/RoomInternalHooksServiceSender'; +import { FederationRoomServiceSenderEE } from '../application/sender/room/RoomServiceSender'; import type { IFederationBridgeEE } from '../domain/IFederationBridge'; import { MatrixBridgeEE } from './matrix/Bridge'; import { RocketChatNotificationAdapter } from './rocket-chat/adapters/Notification'; @@ -12,24 +12,34 @@ import { RocketChatRoomAdapterEE } from './rocket-chat/adapters/Room'; import { RocketChatUserAdapterEE } from './rocket-chat/adapters/User'; import { FederationRoomSenderConverterEE } from './rocket-chat/converters/RoomSender'; import { FederationHooksEE } from './rocket-chat/hooks'; +import type { RocketChatMessageAdapter } from '../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/Message'; +import type { RocketChatFileAdapter } from '../../../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/File'; export class FederationFactoryEE { public static buildRoomServiceSender( rocketRoomAdapter: RocketChatRoomAdapterEE, rocketUserAdapter: RocketChatUserAdapterEE, rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketFiledapter: RocketChatFileAdapter, bridge: IFederationBridgeEE, ): FederationRoomServiceSenderEE { - return new FederationRoomServiceSenderEE(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, bridge); + return new FederationRoomServiceSenderEE(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, rocketFiledapter, bridge); } public static buildRoomInternalHooksServiceSender( rocketRoomAdapter: RocketChatRoomAdapterEE, rocketUserAdapter: RocketChatUserAdapterEE, rocketSettingsAdapter: RocketChatSettingsAdapter, + rocketMessageAdapter: RocketChatMessageAdapter, bridge: IFederationBridgeEE, ): FederationRoomInternalHooksServiceSender { - return new FederationRoomInternalHooksServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, bridge); + return new FederationRoomInternalHooksServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + rocketMessageAdapter, + bridge, + ); } public static buildDMRoomInternalHooksServiceSender( diff --git a/apps/meteor/ee/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/ee/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts index cfe50007520..af77fe33232 100644 --- a/apps/meteor/ee/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/ee/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts @@ -1,4 +1,5 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../../../../lib/callbacks'; @@ -7,7 +8,7 @@ export class FederationHooksEE { callbacks.add( 'federation.afterCreateFederatedRoom', (room: IRoom, { owner, originalMemberList }): void => { - if (!room.federated) { + if (!room || !isRoomFederated(room)) { return; } Promise.await(callback(room, owner, originalMemberList)); @@ -21,7 +22,7 @@ export class FederationHooksEE { callbacks.add( 'afterAddedToRoom', (params: { user: IUser; inviter: IUser }, room: IRoom): void => { - if (!room.federated) { + if (!room || !isRoomFederated(room)) { return; } Promise.await(callback(room, params.inviter, [params.user])); @@ -54,7 +55,7 @@ export class FederationHooksEE { callbacks.add( 'federation.beforeAddUserAToRoom', (params: { user: IUser | string }, room: IRoom): void => { - if (!room.federated) { + if (!room || !isRoomFederated(room)) { return; } Promise.await(callback(params.user, room)); diff --git a/apps/meteor/ee/tests/unit/app/federation-v2/server/application/DMRoomInternalHooksServiceSender.spec.ts b/apps/meteor/ee/tests/unit/app/federation-v2/server/application/DMRoomInternalHooksServiceSender.spec.ts index ded988d12d5..bb8cd9cd087 100644 --- a/apps/meteor/ee/tests/unit/app/federation-v2/server/application/DMRoomInternalHooksServiceSender.spec.ts +++ b/apps/meteor/ee/tests/unit/app/federation-v2/server/application/DMRoomInternalHooksServiceSender.spec.ts @@ -26,7 +26,7 @@ const { FederatedUserEE } = proxyquire.noCallThru().load('../../../../../../app/ }); const { FederationDMRoomInternalHooksServiceSender } = proxyquire .noCallThru() - .load('../../../../../../app/federation-v2/server/application/sender/DMRoomInternalHooksServiceSender', { + .load('../../../../../../app/federation-v2/server/application/sender/room/DMRoomInternalHooksServiceSender', { mongodb: { 'ObjectId': class ObjectId { toHexString(): string { @@ -49,6 +49,7 @@ describe('FederationEE - Application - FederationDMRoomInternalHooksServiceSende getInternalUserById: sinon.stub(), getFederatedUserByInternalUsername: sinon.stub(), createLocalUser: sinon.stub(), + getInternalUserByUsername: sinon.stub(), }; const settingsAdapter = { getHomeServerDomain: sinon.stub().returns('localDomain'), @@ -79,6 +80,7 @@ describe('FederationEE - Application - FederationDMRoomInternalHooksServiceSende userAdapter.createFederatedUser.reset(); userAdapter.getFederatedUserByInternalUsername.reset(); userAdapter.createLocalUser.reset(); + userAdapter.getInternalUserByUsername.reset(); bridge.extractHomeserverOrigin.reset(); bridge.createUser.reset(); bridge.createDirectMessageRoom.reset(); @@ -121,7 +123,6 @@ describe('FederationEE - Application - FederationDMRoomInternalHooksServiceSende username: 'username', existsOnlyOnProxyServer: true, }); - console.log({ inviter }); expect(bridge.createUser.calledWith('username', 'name', 'localDomain')).to.be.true; expect(userAdapter.createFederatedUser.calledWith(inviter)).to.be.true; }); diff --git a/apps/meteor/ee/tests/unit/app/federation-v2/server/application/RoomInternalHooksServiceSender.spec.ts b/apps/meteor/ee/tests/unit/app/federation-v2/server/application/RoomInternalHooksServiceSender.spec.ts index 3ba9e26dcb5..f645423113e 100644 --- a/apps/meteor/ee/tests/unit/app/federation-v2/server/application/RoomInternalHooksServiceSender.spec.ts +++ b/apps/meteor/ee/tests/unit/app/federation-v2/server/application/RoomInternalHooksServiceSender.spec.ts @@ -26,7 +26,7 @@ const { FederatedUserEE } = proxyquire.noCallThru().load('../../../../../../app/ }); const { FederationRoomInternalHooksServiceSender } = proxyquire .noCallThru() - .load('../../../../../../app/federation-v2/server/application/sender/RoomInternalHooksServiceSender', { + .load('../../../../../../app/federation-v2/server/application/sender/room/RoomInternalHooksServiceSender', { mongodb: { 'ObjectId': class ObjectId { toHexString(): string { @@ -50,10 +50,12 @@ describe('FederationEE - Application - FederationRoomInternalHooksServiceSender' getInternalUserById: sinon.stub(), getFederatedUserByInternalUsername: sinon.stub(), createLocalUser: sinon.stub(), + getInternalUserByUsername: sinon.stub(), }; const settingsAdapter = { getHomeServerDomain: sinon.stub().returns('localDomain'), }; + const messageAdapter = {}; const bridge = { getUserProfileInformation: sinon.stub().resolves({}), extractHomeserverOrigin: sinon.stub(), @@ -74,7 +76,13 @@ describe('FederationEE - Application - FederationRoomInternalHooksServiceSender' ]; beforeEach(() => { - service = new FederationRoomInternalHooksServiceSender(roomAdapter as any, userAdapter as any, settingsAdapter as any, bridge as any); + service = new FederationRoomInternalHooksServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + messageAdapter as any, + bridge as any, + ); }); afterEach(() => { @@ -86,6 +94,7 @@ describe('FederationEE - Application - FederationRoomInternalHooksServiceSender' userAdapter.createFederatedUser.reset(); userAdapter.getFederatedUserByInternalUsername.reset(); userAdapter.createLocalUser.reset(); + userAdapter.getInternalUserByUsername.reset(); bridge.extractHomeserverOrigin.reset(); bridge.createUser.reset(); bridge.createRoom.reset(); diff --git a/apps/meteor/ee/tests/unit/app/federation-v2/server/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/ee/tests/unit/app/federation-v2/server/infrastructure/rocket-chat/hooks/hooks.spec.ts index 3450bec636c..ec6a797b1e2 100644 --- a/apps/meteor/ee/tests/unit/app/federation-v2/server/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/ee/tests/unit/app/federation-v2/server/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -1,49 +1,50 @@ -// /* eslint-disable */ -// import proxyquire from 'proxyquire'; -// import { expect } from 'chai'; -// import sinon from 'sinon'; +/* eslint-disable */ +import proxyquire from 'proxyquire'; +import { expect } from 'chai'; +import sinon from 'sinon'; -// const remove = sinon.stub(); -// proxyquire.noCallThru().load('../../../../../../../../../lib/callbacks', { -// 'meteor/meteor': {}, -// 'meteor/random': { -// Random: { -// id: () => 1, -// }, -// }, -// callbacks: { -// remove, -// }, -// }); +const remove = sinon.stub(); +const { FederationHooksEE } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/hooks', { + 'meteor/meteor': { + '@global': true, + }, + 'meteor/random': { + Random: { + id: () => 1, + }, + '@global': true, + }, + '../../../../../../../lib/callbacks': { + callbacks: { remove }, + }, +}); -// import { FederationHooksEE } from '../../../../../../../../app/federation-v2/server/infrastructure/rocket-chat/hooks'; - -// describe.only('FederationEE - Infrastructure - RocketChat - Hooks', () => { -// describe('#removeAll()', () => { -// it('should remove the specific validation for EE environments', () => { -// FederationHooksEE.removeAll(); -// expect(remove.callCount).to.be.equal(7); -// expect( -// remove.getCall(0).calledWith('beforeCreateDirectRoom', 'federation-v2-before-create-direct-message-room'), -// ).to.be.equal(true); -// expect( -// remove.getCall(1).calledWith('afterCreateDirectRoom', 'federation-v2-after-create-direct-message-room'), -// ).to.be.equal(true); -// expect( -// remove.getCall(2).calledWith('afterAddedToRoom', 'federation-v2-after-add-users-to-a-room'), -// ).to.be.equal(true); -// expect( -// remove.getCall(3).calledWith('federation.afterCreateFederatedRoom', 'federation-v2-after-create-room'), -// ).to.be.equal(true); -// expect( -// remove.getCall(4).calledWith('federation.beforeAddUserAToRoom', 'federation-v2-before-add-user-to-the-room'), -// ).to.be.equal(true); -// expect( -// remove.getCall(5).calledWith('afterRoomNameChange', 'federation-v2-after-room-name-changed'), -// ).to.be.equal(true); -// expect( -// remove.getCall(6).calledWith('afterRoomTopicChange', 'federation-v2-after-room-topic-changed'), -// ).to.be.equal(true); -// }); -// }); -// }); +describe('FederationEE - Infrastructure - RocketChat - Hooks', () => { + describe('#removeAll()', () => { + it('should remove the specific validation for EE environments', () => { + FederationHooksEE.removeAll(); + expect(remove.callCount).to.be.equal(7); + expect( + remove.getCall(0).calledWith('beforeCreateDirectRoom', 'federation-v2-before-create-direct-message-room'), + ).to.be.equal(true); + expect( + remove.getCall(1).calledWith('afterCreateDirectRoom', 'federation-v2-after-create-direct-message-room'), + ).to.be.equal(true); + expect( + remove.getCall(2).calledWith('afterAddedToRoom', 'federation-v2-after-add-users-to-a-room'), + ).to.be.equal(true); + expect( + remove.getCall(3).calledWith('federation.afterCreateFederatedRoom', 'federation-v2-after-create-room'), + ).to.be.equal(true); + expect( + remove.getCall(4).calledWith('federation.beforeAddUserAToRoom', 'federation-v2-before-add-user-to-the-room'), + ).to.be.equal(true); + expect( + remove.getCall(5).calledWith('afterRoomNameChange', 'federation-v2-after-room-name-changed'), + ).to.be.equal(true); + expect( + remove.getCall(6).calledWith('afterRoomTopicChange', 'federation-v2-after-room-topic-changed'), + ).to.be.equal(true); + }); + }); +}); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 8ea3e46dd04..3da9b99602f 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -66,6 +66,11 @@ type EventLikeCallbackSignatures = { 'federation.afterCreateFederatedRoom': (room: IRoom, second: { owner: IUser; originalMemberList: string[] }) => void; 'beforeCreateDirectRoom': (members: IUser[]) => void; 'federation.beforeCreateDirectMessage': (members: IUser[]) => void; + 'afterSetReaction': (message: IMessage, { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean }) => void; + 'afterUnsetReaction': ( + message: IMessage, + { user, reaction }: { user: IUser; reaction: string; shouldReact: boolean; oldMessage: IMessage }, + ) => void; 'federation.beforeAddUserAToRoom': (params: { user: IUser | string; inviter: IUser }, room: IRoom) => void; 'onJoinVideoConference': (callId: VideoConference['_id'], userId?: IUser['_id']) => Promise<void>; }; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 54a22b14c80..a5c8b9ef323 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -98,6 +98,7 @@ "@types/cssom": "^0.4.1", "@types/dompurify": "^2.3.3", "@types/ejson": "^2.2.0", + "@types/emojione": "^2.2.6", "@types/express": "^4.17.13", "@types/express-rate-limit": "^5.1.3", "@types/fibers": "^3.1.1", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 2d145afb85d..16d31389924 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -2095,8 +2095,6 @@ "files": "Dateien", "Files": "Dateien", "Files_only": "Entferne nur die angehängten Dateien, behalte Nachrichten", - "Files_unavailable_for_federation": "Dateien sind für Verbundräume nicht verfügbar", - "files_pruned": "Dateien gelöscht", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index fc4904d5f98..5e1b906f408 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2139,8 +2139,6 @@ "files": "files", "Files": "Files", "Files_only": "Only remove the attached files, keep messages", - "Files_unavailable_for_federation": "Files are unavailable for Federated rooms", - "files_pruned": "files pruned", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2148,7 +2146,6 @@ "FileUpload_Description": "Configure file upload and storage.", "FileUpload_Cannot_preview_file": "Cannot preview file", "FileUpload_Disabled": "File uploads are disabled.", - "FileUpload_Disabled_for_federation": "File uploads are disabled for Federated rooms.", "FileUpload_Enable_json_web_token_for_files": "Enable Json Web Tokens protection to file uploads", "FileUpload_Enable_json_web_token_for_files_description": "Appends a JWT to uploaded files urls", "FileUpload_Enabled": "File Uploads Enabled", diff --git a/apps/meteor/server/models/raw/Messages.ts b/apps/meteor/server/models/raw/Messages.ts index 3ff84b439fe..e1e85e97f21 100644 --- a/apps/meteor/server/models/raw/Messages.ts +++ b/apps/meteor/server/models/raw/Messages.ts @@ -1,10 +1,21 @@ import type { ILivechatDepartment, IMessage, IRoom, IUser, MessageTypesValues, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, IMessagesModel } from '@rocket.chat/model-typings'; import type { PaginatedRequest } from '@rocket.chat/rest-typings'; -import type { AggregationCursor, Collection, CountDocumentsOptions, AggregateOptions, FindCursor, Db, Filter, FindOptions } from 'mongodb'; +import type { + AggregationCursor, + Collection, + CountDocumentsOptions, + AggregateOptions, + FindCursor, + Db, + Filter, + FindOptions, + IndexDescription, +} from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { BaseRaw } from './BaseRaw'; +import { escapeExternalFederationEventId } from '../../../app/federation-v2/server/infrastructure/rocket-chat/adapters/MessageConverter'; // @ts-ignore Circular reference on field 'attachments' export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel { @@ -12,6 +23,10 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel { super(db, 'message', trash); } + protected modelIndexes(): IndexDescription[] { + return [{ key: { 'federation.eventId': 1 }, sparse: true }]; + } + findVisibleByMentionAndRoomId( username: IUser['username'], rid: IRoom['_id'], @@ -338,4 +353,64 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel { return this.find(query, options); } + + async setFederationReactionEventId(username: string, _id: string, reaction: string, federationEventId: string): Promise<void> { + await this.updateOne( + { _id }, + { + $set: { + [`reactions.${reaction}.federationReactionEventIds.${escapeExternalFederationEventId(federationEventId)}`]: username, + }, + }, + ); + } + + async unsetFederationReactionEventId(federationEventId: string, _id: string, reaction: string): Promise<void> { + await this.updateOne( + { _id }, + { + $unset: { + [`reactions.${reaction}.federationReactionEventIds.${escapeExternalFederationEventId(federationEventId)}`]: 1, + }, + }, + ); + } + + async findOneByFederationId(federationEventId: string): Promise<IMessage | null> { + return this.findOne({ 'federation.eventId': federationEventId }); + } + + async findOneByFederationIdAndUsernameOnReactions(federationEventId: string, username: string): Promise<IMessage | null> { + return ( + await this.col + .aggregate([ + { + $match: { + t: { $ne: 'rm' }, + }, + }, + { + $project: { + document: '$$ROOT', + reactions: { $objectToArray: '$reactions' }, + }, + }, + { + $unwind: { + path: '$reactions', + }, + }, + { + $match: { + $and: [ + { 'reactions.v.usernames': { $in: [username] } }, + { [`reactions.v.federationReactionEventIds.${escapeExternalFederationEventId(federationEventId)}`]: username }, + ], + }, + }, + { $replaceRoot: { newRoot: '$document' } }, + ]) + .toArray() + )[0] as IMessage; + } } diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/Federation.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/Federation.spec.ts index f29507b90c2..a483ddcfc0b 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/Federation.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/Federation.spec.ts @@ -39,4 +39,16 @@ describe('Federation[Server] - Federation', () => { expect(Federation.isAFederatedUsername('user:domain.com')).to.be.false; }); }); + + describe('#escapeExternalFederationId()', () => { + it('should replace all "$" with "__sign__"', () => { + expect(Federation.escapeExternalFederationEventId('$stri$ng')).to.be.equal('__sign__stri__sign__ng'); + }); + }); + + describe('#unescapeExternalFederationEventId()', () => { + it('should replace all "__sign__" with "$"', () => { + expect(Federation.unescapeExternalFederationEventId('__sign__stri__sign__ng')).to.be.equal('$stri$ng'); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/MessageServiceListener.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/MessageServiceListener.spec.ts new file mode 100644 index 00000000000..79509980ed8 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/MessageServiceListener.spec.ts @@ -0,0 +1,155 @@ +/* eslint-disable import/first */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import proxyquire from 'proxyquire'; + +const { FederatedUser } = proxyquire.noCallThru().load('../../../../../../../app/federation-v2/server/domain/FederatedUser', { + mongodb: { + 'ObjectId': class ObjectId { + toHexString(): string { + return 'hexString'; + } + }, + '@global': true, + }, +}); + +const { FederatedRoom } = proxyquire.noCallThru().load('../../../../../../../app/federation-v2/server/domain/FederatedRoom', { + mongodb: { + 'ObjectId': class ObjectId { + toHexString(): string { + return 'hexString'; + } + }, + '@global': true, + }, +}); + +import { FederationMessageServiceListener } from '../../../../../../../app/federation-v2/server/application/MessageServiceListener'; + +describe('Federation - Application - FederationMessageServiceListener', () => { + let service: FederationMessageServiceListener; + const roomAdapter = { + getFederatedRoomByExternalId: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByExternalId: sinon.stub(), + }; + const messageAdapter = { + getMessageByFederationId: sinon.stub(), + reactToMessage: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub().returns('localDomain'), + }; + + beforeEach(() => { + service = new FederationMessageServiceListener( + roomAdapter as any, + userAdapter as any, + messageAdapter as any, + settingsAdapter as any, + {} as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByExternalId.reset(); + userAdapter.getFederatedUserByExternalId.reset(); + messageAdapter.getMessageByFederationId.reset(); + messageAdapter.reactToMessage.reset(); + }); + + describe('#onMessageReaction()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should NOT react to the message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.onMessageReaction({ + externalReactedEventId: 'externalReactedEventId', + } as any); + + expect(messageAdapter.reactToMessage.called).to.be.false; + }); + + it('should NOT react to the message if the user does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.onMessageReaction({ + externalReactedEventId: 'externalReactedEventId', + } as any); + + expect(messageAdapter.reactToMessage.called).to.be.false; + }); + + it('should NOT react to the message if the message does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(undefined); + await service.onMessageReaction({ + externalReactedEventId: 'externalReactedEventId', + } as any); + + expect(messageAdapter.reactToMessage.called).to.be.false; + }); + + it('should NOT react to the message if it is not a Matrix federation one', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves({ msg: 'newMessageText' }); + await service.onMessageReaction({ + externalReactedEventId: 'externalReactedEventId', + } as any); + + expect(messageAdapter.reactToMessage.called).to.be.false; + }); + + it('should NOT react to the message if the user already reacted to it', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves({ + msg: 'newMessageText', + federation: { eventId: 'eventId' }, + reactions: { + ':emoji:': { + usernames: ['normalizedInviterId'], + }, + }, + }); + await service.onMessageReaction({ + externalReactedEventId: 'externalReactedEventId', + emoji: ':emoji:', + } as any); + + expect(messageAdapter.reactToMessage.called).to.be.false; + }); + + it('should react to the message', async () => { + const message = { + msg: 'newMessageText', + federation: { eventId: 'eventId' }, + reactions: { + ':emoji:': { + usernames: [], + }, + }, + }; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(message); + await service.onMessageReaction({ + externalEventId: 'externalEventId', + externalReactedEventId: 'externalReactedEventId', + emoji: ':emoji:', + } as any); + + expect(messageAdapter.reactToMessage.calledWith(user, message, ':emoji:', 'externalEventId')).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/RoomServiceListener.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/RoomServiceListener.spec.ts index eadf3d9f45f..2f3c447e645 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/RoomServiceListener.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/RoomServiceListener.spec.ts @@ -62,17 +62,28 @@ describe('Federation - Application - FederationRoomServiceListener', () => { const userAdapter = { getFederatedUserByExternalId: sinon.stub(), createFederatedUser: sinon.stub(), + getInternalUserByUsername: sinon.stub(), }; const messageAdapter = { sendMessage: sinon.stub(), + sendFileMessage: sinon.stub(), + deleteMessage: sinon.stub(), + getMessageByFederationId: sinon.stub(), + editMessage: sinon.stub(), + findOneByFederationIdOnReactions: sinon.stub(), + unreactToMessage: sinon.stub(), }; const settingsAdapter = { getHomeServerDomain: sinon.stub().returns('localDomain'), }; + const fileAdapter = { + uploadFile: sinon.stub(), + }; const bridge = { getUserProfileInformation: sinon.stub().resolves({}), extractHomeserverOrigin: sinon.stub().returns('localDomain'), joinRoom: sinon.stub(), + getReadStreamForFileFromUrl: sinon.stub(), }; beforeEach(() => { @@ -81,6 +92,7 @@ describe('Federation - Application - FederationRoomServiceListener', () => { userAdapter as any, messageAdapter as any, settingsAdapter as any, + fileAdapter as any, bridge as any, ); }); @@ -101,9 +113,18 @@ describe('Federation - Application - FederationRoomServiceListener', () => { roomAdapter.addUserToRoom.reset(); userAdapter.getFederatedUserByExternalId.reset(); userAdapter.createFederatedUser.reset(); + userAdapter.getInternalUserByUsername.reset(); messageAdapter.sendMessage.reset(); + messageAdapter.sendFileMessage.reset(); + messageAdapter.deleteMessage.reset(); + messageAdapter.getMessageByFederationId.reset(); + messageAdapter.editMessage.reset(); + messageAdapter.unreactToMessage.reset(); + messageAdapter.findOneByFederationIdOnReactions.reset(); bridge.extractHomeserverOrigin.reset(); bridge.joinRoom.reset(); + bridge.getReadStreamForFileFromUrl.reset(); + fileAdapter.uploadFile.reset(); }); describe('#onCreateRoom()', () => { @@ -526,6 +547,53 @@ describe('Federation - Application - FederationRoomServiceListener', () => { }); }); + describe('#onExternalFileMessageReceived()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + it('should NOT send a message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.onExternalFileMessageReceived({ + messageText: 'text', + } as any); + + expect(messageAdapter.sendFileMessage.called).to.be.false; + }); + + it('should NOT send a message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves({} as any); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.onExternalFileMessageReceived({ + messageText: 'text', + } as any); + + expect(messageAdapter.sendFileMessage.called).to.be.false; + }); + + it('should send a message if the room and the sender already exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + bridge.getReadStreamForFileFromUrl.resolves(); + const files = [{ id: 'fileId', name: 'filename' }]; + const attachments = ['attachment', 'attachment2']; + fileAdapter.uploadFile.resolves({ files, attachments } as any); + + await service.onExternalFileMessageReceived({ + messageBody: { + filename: 'filename', + size: 12, + mimetype: 'mimetype', + url: 'url', + }, + } as any); + + expect(messageAdapter.sendFileMessage.calledWith(user, room, files, attachments)).to.be.true; + }); + }); + describe('#onExternalMessageReceived()', () => { it('should NOT send a message if the room does not exists', async () => { roomAdapter.getFederatedRoomByExternalId.resolves(undefined); @@ -667,4 +735,167 @@ describe('Federation - Application - FederationRoomServiceListener', () => { expect(roomAdapter.updateRoomTopic.calledWith(room, user)).to.be.true; }); }); + + describe('#onRedactEvent()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should NOT delete the message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.called).to.be.false; + expect(messageAdapter.unreactToMessage.called).to.be.false; + }); + + it('should NOT delete the message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.called).to.be.false; + expect(messageAdapter.unreactToMessage.called).to.be.false; + }); + + it('should NOT delete the message if the message does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(undefined); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.called).to.be.false; + expect(messageAdapter.unreactToMessage.called).to.be.false; + }); + + it('should delete the message if its a raw text redact handler', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves({ msg: 'msg' }); + messageAdapter.findOneByFederationIdOnReactions.resolves(undefined); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.calledWith({ msg: 'msg' }, user)).to.be.true; + expect(messageAdapter.unreactToMessage.called).to.be.false; + }); + + it('should NOT unreact if the message was not reacted before by the user', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(undefined); + messageAdapter.findOneByFederationIdOnReactions.resolves({ + msg: 'msg', + reactions: { + reaction: { + federationReactionEventIds: {}, + usernames: [], + }, + }, + }); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.called).to.be.false; + expect(messageAdapter.unreactToMessage.called).to.be.false; + }); + + it('should unreact if the message was reacted before by the user', async () => { + const message = { + msg: 'msg', + reactions: { + reaction: { + federationReactionEventIds: { + redactsEvent: user.getUsername(), + }, + usernames: [user.getUsername()], + }, + }, + }; + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(undefined); + messageAdapter.findOneByFederationIdOnReactions.resolves(message); + await service.onRedactEvent({ + redactsEvent: 'redactsEvent', + } as any); + + expect(messageAdapter.deleteMessage.called).to.be.false; + expect(messageAdapter.unreactToMessage.calledWith(user, message, 'reaction', 'redactsEvent')).to.be.true; + }); + }); + + describe('#onExternalMessageEditedReceived()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: false, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should NOT update the message if the room does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(undefined); + await service.onExternalMessageEditedReceived({ + editsEvent: 'editsEvent', + } as any); + + expect(messageAdapter.editMessage.called).to.be.false; + }); + + it('should NOT update the message if the sender does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(undefined); + await service.onExternalMessageEditedReceived({ + editsEvent: 'editsEvent', + } as any); + + expect(messageAdapter.editMessage.called).to.be.false; + }); + + it('should NOT update the message if the message does not exists', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves(undefined); + await service.onExternalMessageEditedReceived({ + editsEvent: 'editsEvent', + } as any); + + expect(messageAdapter.editMessage.called).to.be.false; + }); + + it('should NOT update the message if the content of the message is equal of the oldest one', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves({ msg: 'newMessageText' }); + await service.onExternalMessageEditedReceived({ + editsEvent: 'editsEvent', + newMessageText: 'newMessageText', + } as any); + + expect(messageAdapter.editMessage.called).to.be.false; + }); + + it('should update the message', async () => { + roomAdapter.getFederatedRoomByExternalId.resolves(room); + userAdapter.getFederatedUserByExternalId.resolves(user); + messageAdapter.getMessageByFederationId.resolves({ msg: 'differentOne' }); + await service.onExternalMessageEditedReceived({ + editsEvent: 'editsEvent', + newMessageText: 'newMessageText', + } as any); + + expect(messageAdapter.editMessage.calledWith(user, 'newMessageText', { msg: 'differentOne' })).to.be.true; + }); + }); }); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageSenders.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageSenders.spec.ts new file mode 100644 index 00000000000..4f28f197188 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageSenders.spec.ts @@ -0,0 +1,91 @@ +/* eslint-disable import/first */ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { getExternalMessageSender } from '../../../../../../../../app/federation-v2/server/application/sender/MessageSenders'; + +describe('Federation - Application - Message Senders', () => { + const bridge = { + sendMessage: sinon.stub(), + sendMessageFileToRoom: sinon.stub(), + }; + const fileAdapter = { + getBufferFromFileRecord: sinon.stub(), + getFileRecordById: sinon.stub(), + extractMetadataFromFile: sinon.stub(), + }; + + afterEach(() => { + bridge.sendMessage.reset(); + bridge.sendMessageFileToRoom.reset(); + fileAdapter.getBufferFromFileRecord.reset(); + fileAdapter.getFileRecordById.reset(); + fileAdapter.extractMetadataFromFile.reset(); + }); + + describe('TextExternalMessageSender', () => { + const roomId = 'roomId'; + const senderId = 'senderId'; + const message = { msg: 'text' } as any; + + describe('#sendMessage()', () => { + it('should send a message through the bridge', async () => { + await getExternalMessageSender({} as any, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); + expect(bridge.sendMessage.calledWith(roomId, senderId, message.msg)).to.be.true; + }); + }); + }); + + describe('FileExternalMessageSender', () => { + const roomId = 'roomId'; + const senderId = 'senderId'; + const message = { msg: 'text', files: [{ _id: 'fileId' }] } as any; + + describe('#sendMessage()', () => { + it('should not upload the file to the bridge if the file does not exists', async () => { + fileAdapter.getFileRecordById.resolves(undefined); + await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); + expect(bridge.sendMessageFileToRoom.called).to.be.false; + expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; + }); + + it('should not upload the file to the bridge if the file size does not exists', async () => { + fileAdapter.getFileRecordById.resolves({}); + await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); + expect(bridge.sendMessageFileToRoom.called).to.be.false; + expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; + }); + + it('should not upload the file to the bridge if the file type does not exists', async () => { + fileAdapter.getFileRecordById.resolves({ size: 12 }); + await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); + expect(bridge.sendMessageFileToRoom.called).to.be.false; + expect(fileAdapter.getBufferFromFileRecord.called).to.be.false; + }); + + it('should send a message (upload the file) through the bridge', async () => { + fileAdapter.getFileRecordById.resolves({ name: 'filename', size: 12, type: 'image/png' }); + fileAdapter.getBufferFromFileRecord.resolves({ buffer: 'buffer' }); + await getExternalMessageSender(message, bridge as any, fileAdapter as any).sendMessage(roomId, senderId, message); + expect(fileAdapter.getBufferFromFileRecord.calledWith({ name: 'filename', size: 12, type: 'image/png' })).to.be.true; + expect( + bridge.sendMessageFileToRoom.calledWith( + roomId, + senderId, + { buffer: 'buffer' }, + { + filename: 'filename', + fileSize: 12, + mimeType: 'image/png', + metadata: { + width: undefined, + height: undefined, + format: undefined, + }, + }, + ), + ).to.be.true; + }); + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageServiceSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageServiceSender.spec.ts new file mode 100644 index 00000000000..96ad9d534ec --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/MessageServiceSender.spec.ts @@ -0,0 +1,286 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import proxyquire from 'proxyquire'; + +import { FederationMessageServiceSender } from '../../../../../../../../app/federation-v2/server/application/sender/MessageServiceSender'; + +const { FederatedUser } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/domain/FederatedUser', { + mongodb: { + 'ObjectId': class ObjectId { + toHexString(): string { + return 'hexString'; + } + }, + '@global': true, + }, +}); + +const { FederatedRoom } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/domain/FederatedRoom', { + mongodb: { + 'ObjectId': class ObjectId { + toHexString(): string { + return 'hexString'; + } + }, + '@global': true, + }, +}); + +describe('Federation - Application - FederationMessageServiceSender', () => { + let service: FederationMessageServiceSender; + const roomAdapter = { + getFederatedRoomByInternalId: sinon.stub(), + }; + const userAdapter = { + getFederatedUserByInternalId: sinon.stub(), + }; + const settingsAdapter = { + getHomeServerDomain: sinon.stub().returns('localDomain'), + }; + const messageAdapter = { + setExternalFederationEventOnMessage: sinon.stub(), + unsetExternalFederationEventOnMessage: sinon.stub(), + }; + const bridge = { + extractHomeserverOrigin: sinon.stub(), + sendMessageReaction: sinon.stub(), + redactEvent: sinon.stub(), + }; + + beforeEach(() => { + service = new FederationMessageServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + messageAdapter as any, + bridge as any, + ); + }); + + afterEach(() => { + roomAdapter.getFederatedRoomByInternalId.reset(); + userAdapter.getFederatedUserByInternalId.reset(); + bridge.extractHomeserverOrigin.reset(); + messageAdapter.setExternalFederationEventOnMessage.reset(); + messageAdapter.unsetExternalFederationEventOnMessage.reset(); + bridge.sendMessageReaction.reset(); + bridge.redactEvent.reset(); + }); + + describe('#sendExternalMessageReaction()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: true, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should not send the reaction if the internal message does not exists', async () => { + await service.sendExternalMessageReaction(undefined as any, {} as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the reaction if the internal user does not exists', async () => { + await service.sendExternalMessageReaction({} as any, undefined as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the reaction if the internal user id does not exists', async () => { + await service.sendExternalMessageReaction({} as any, {} as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the reaction if the internal message room id does not exists', async () => { + await service.sendExternalMessageReaction({} as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the reaction the user does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(undefined); + await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the reaction the room does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the reaction the the message is not from matrix federation', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + await service.sendExternalMessageReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the reaction if the user is not from the same home server', async () => { + bridge.extractHomeserverOrigin.returns('externalDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.sendExternalMessageReaction( + { rid: 'roomId', federation: { eventId: 'eventId' } } as any, + { _id: 'id' } as any, + 'reaction', + ); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should send the reaction', async () => { + bridge.extractHomeserverOrigin.returns('localDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + bridge.sendMessageReaction.resolves('returnedEventId'); + await service.sendExternalMessageReaction( + { rid: 'roomId', federation: { eventId: 'eventId' } } as any, + { _id: 'id' } as any, + 'reaction', + ); + + expect(bridge.sendMessageReaction.calledWith(room.getExternalId(), user.getExternalId(), 'eventId', 'reaction')).to.be.true; + expect( + messageAdapter.setExternalFederationEventOnMessage.calledWith( + user.getUsername(), + { rid: 'roomId', federation: { eventId: 'eventId' } }, + 'reaction', + 'returnedEventId', + ), + ).to.be.true; + }); + }); + + describe('#sendExternalMessageUnReaction()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: true, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should not send the unreaction if the internal message does not exists', async () => { + await service.sendExternalMessageUnReaction(undefined as any, {} as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the unreaction if the internal user does not exists', async () => { + await service.sendExternalMessageUnReaction({} as any, undefined as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the unreaction if the internal user id does not exists', async () => { + await service.sendExternalMessageUnReaction({} as any, {} as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the unreaction if the internal message room id does not exists', async () => { + await service.sendExternalMessageUnReaction({} as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + expect(userAdapter.getFederatedUserByInternalId.called).to.be.false; + }); + + it('should not send the unreaction the user does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(undefined); + await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the unreaction the room does not exists', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the unreaction the the message is not from matrix federation', async () => { + userAdapter.getFederatedUserByInternalId.resolves(user); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + await service.sendExternalMessageUnReaction({ rid: 'roomId' } as any, { _id: 'id' } as any, 'reaction'); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the unreaction if the user is not from the same home server', async () => { + bridge.extractHomeserverOrigin.onCall(0).returns('localDomain'); + bridge.extractHomeserverOrigin.onCall(1).returns('externalDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.sendExternalMessageUnReaction( + { rid: 'roomId', federation: { eventId: 'eventId' } } as any, + { _id: 'id' } as any, + 'reaction', + ); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the unreaction if the user is not from the same home server', async () => { + bridge.extractHomeserverOrigin.returns('externalDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.sendExternalMessageUnReaction( + { rid: 'roomId', federation: { eventId: 'eventId' } } as any, + { _id: 'id' } as any, + 'reaction', + ); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should not send the unreaction if there is no existing reaction', async () => { + bridge.extractHomeserverOrigin.returns('localDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.sendExternalMessageUnReaction( + { rid: 'roomId', federation: { eventId: 'eventId' } } as any, + { _id: 'id' } as any, + 'reaction', + ); + + expect(bridge.sendMessageReaction.called).to.be.false; + }); + + it('should send the unreaction', async () => { + const message = { + rid: 'roomId', + federation: { eventId: 'eventId' }, + reactions: { + reaction: { + federationReactionEventIds: { + eventId: user.getUsername(), + }, + }, + }, + } as any; + bridge.extractHomeserverOrigin.returns('localDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.sendExternalMessageUnReaction(message, { _id: 'id', username: user.getUsername() } as any, 'reaction'); + + expect(bridge.redactEvent.calledWith(room.getExternalId(), user.getExternalId(), 'eventId')).to.be.true; + expect(messageAdapter.unsetExternalFederationEventOnMessage.calledWith('eventId', message, 'reaction')).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/RoomServiceSender.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/RoomServiceSender.spec.ts index bd89946fe2e..b9d5606fbc7 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/RoomServiceSender.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/application/sender/RoomServiceSender.spec.ts @@ -3,10 +3,11 @@ import { expect } from 'chai'; import sinon from 'sinon'; import proxyquire from 'proxyquire'; +const stub = sinon.stub(); const { FederationRoomServiceSender } = proxyquire .noCallThru() .load('../../../../../../../../app/federation-v2/server/application/sender/RoomServiceSender', { - mongodb: { + 'mongodb': { 'ObjectId': class ObjectId { toHexString(): string { return 'hexString'; @@ -14,6 +15,9 @@ const { FederationRoomServiceSender } = proxyquire }, '@global': true, }, + './MessageSenders': { + getExternalMessageSender: () => ({ sendMessage: stub }), + }, }); const { FederatedUser } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/domain/FederatedUser', { @@ -54,10 +58,12 @@ describe('Federation - Application - FederationRoomServiceSender', () => { createFederatedUser: sinon.stub(), getInternalUserById: sinon.stub(), getFederatedUserByInternalUsername: sinon.stub(), + getInternalUserByUsername: sinon.stub(), }; const settingsAdapter = { getHomeServerDomain: sinon.stub().returns('localDomain'), }; + const fileAdapter = {}; const bridge = { getUserProfileInformation: sinon.stub().resolves({}), extractHomeserverOrigin: sinon.stub(), @@ -68,10 +74,18 @@ describe('Federation - Application - FederationRoomServiceSender', () => { joinRoom: sinon.stub(), leaveRoom: sinon.stub(), kickUserFromRoom: sinon.stub(), + redactEvent: sinon.stub(), + updateMessage: sinon.stub(), }; beforeEach(() => { - service = new FederationRoomServiceSender(roomAdapter as any, userAdapter as any, settingsAdapter as any, bridge as any); + service = new FederationRoomServiceSender( + roomAdapter as any, + userAdapter as any, + settingsAdapter as any, + fileAdapter as any, + bridge as any, + ); }); afterEach(() => { @@ -84,6 +98,7 @@ describe('Federation - Application - FederationRoomServiceSender', () => { userAdapter.getInternalUserById.reset(); userAdapter.createFederatedUser.reset(); userAdapter.getFederatedUserByInternalUsername.reset(); + userAdapter.getInternalUserByUsername.reset(); bridge.extractHomeserverOrigin.reset(); bridge.sendMessage.reset(); bridge.createUser.reset(); @@ -92,6 +107,8 @@ describe('Federation - Application - FederationRoomServiceSender', () => { bridge.joinRoom.reset(); bridge.leaveRoom.reset(); bridge.kickUserFromRoom.reset(); + bridge.redactEvent.reset(); + bridge.updateMessage.reset(); }); describe('#createDirectMessageRoomAndInviteUser()', () => { @@ -405,7 +422,173 @@ describe('Federation - Application - FederationRoomServiceSender', () => { roomAdapter.getFederatedRoomByInternalId.resolves(room); await service.sendExternalMessage({ message: { msg: 'text' } } as any); - expect(bridge.sendMessage.calledWith(room.getExternalId(), user.getExternalId(), 'text')).to.be.true; + expect(stub.calledWith(room.getExternalId(), user.getExternalId(), { msg: 'text' })).to.be.true; + }); + }); + + describe('#afterMessageDeleted()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: true, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should not delete the message remotely if the room does not exists', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + await service.afterMessageDeleted({ msg: 'msg', u: { _id: 'id' } } as any, 'internalRoomId'); + + expect(bridge.redactEvent.called).to.be.false; + }); + + it('should not delete the message remotely if the user does not exists', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(undefined); + await service.afterMessageDeleted({ msg: 'msg', u: { _id: 'id' } } as any, 'internalRoomId'); + + expect(bridge.redactEvent.called).to.be.false; + }); + + it('should not delete the message remotely if the message is not an external one', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageDeleted({ msg: 'msg', u: { _id: 'id' } } as any, 'internalRoomId'); + + expect(bridge.redactEvent.called).to.be.false; + }); + + it('should not delete the message remotely if the message was already deleted (it was just updated to keep the chat history)', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageDeleted( + { + msg: 'msg', + federation: { eventId: 'id' }, + editedAt: new Date(), + editedBy: 'id', + t: 'rm', + u: { _id: 'id' }, + } as any, + 'internalRoomId', + ); + + expect(bridge.redactEvent.called).to.be.false; + }); + + it('should not delete the message remotely if the user is not from the same home server', async () => { + bridge.extractHomeserverOrigin.returns('externalDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageDeleted( + { + msg: 'msg', + federationEventId: 'id', + u: { _id: 'id' }, + } as any, + 'internalRoomId', + ); + + expect(bridge.redactEvent.called).to.be.false; + }); + + it('should delete the message remotely', async () => { + bridge.extractHomeserverOrigin.returns('localDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageDeleted( + { + msg: 'msg', + federation: { eventId: 'federationEventId' }, + u: { _id: 'id' }, + } as any, + 'internalRoomId', + ); + + expect(bridge.redactEvent.calledWith(room.getExternalId(), user.getExternalId(), 'federationEventId')).to.be.true; + }); + }); + + describe('#afterMessageUpdated()', () => { + const user = FederatedUser.createInstance('externalInviterId', { + name: 'normalizedInviterId', + username: 'normalizedInviterId', + existsOnlyOnProxyServer: true, + }); + const room = FederatedRoom.createInstance('externalRoomId', 'normalizedRoomId', user, RoomType.CHANNEL, 'externalRoomName'); + + it('should not update the message remotely if the room does not exists', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(undefined); + await service.afterMessageUpdated({ msg: 'msg' } as any, 'internalRoomId', 'internalUserId'); + + expect(bridge.updateMessage.called).to.be.false; + }); + + it('should not update the message remotely if the user does not exists', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(undefined); + await service.afterMessageUpdated({ msg: 'msg' } as any, 'internalRoomId', 'internalUserId'); + + expect(bridge.updateMessage.called).to.be.false; + }); + + it('should not update the message remotely if the message is not an external one', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageUpdated({ msg: 'msg' } as any, 'internalRoomId', 'internalUserId'); + + expect(bridge.updateMessage.called).to.be.false; + }); + + it('should not update the message remotely if it was updated not by the sender', async () => { + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + await service.afterMessageUpdated( + { msg: 'msg', federation: { eventId: 'federationEventId' }, u: { _id: 'sender' } } as any, + 'internalRoomId', + 'internalUserId', + ); + + expect(bridge.updateMessage.called).to.be.false; + }); + + it('should not update the message remotely if the user is not from the same home server', async () => { + bridge.extractHomeserverOrigin.returns('externalDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + + await service.afterMessageUpdated( + { + msg: 'msg', + editedAt: new Date(), + editedBy: 'id', + federation: { eventId: 'federationEventId' }, + u: { _id: 'internalUserId' }, + } as any, + 'internalRoomId', + 'internalUserId', + ); + + expect(bridge.updateMessage.called).to.be.false; + }); + + it('should update the message remotely', async () => { + bridge.extractHomeserverOrigin.returns('localDomain'); + roomAdapter.getFederatedRoomByInternalId.resolves(room); + userAdapter.getFederatedUserByInternalId.resolves(user); + + await service.afterMessageUpdated( + { + msg: 'msg', + editedAt: new Date(), + editedBy: 'id', + federation: { eventId: 'federationEventId' }, + u: { _id: 'internalUserId' }, + } as any, + 'internalRoomId', + 'internalUserId', + ); + + expect(bridge.updateMessage.calledWith(room.getExternalId(), user.getExternalId(), 'federationEventId', 'msg')).to.be.true; }); }); }); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/domain/FederatedRoom.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/domain/FederatedRoom.spec.ts index 75758eccb85..1a62d6566ab 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/domain/FederatedRoom.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/domain/FederatedRoom.spec.ts @@ -222,6 +222,16 @@ describe('Federation - Domain - FederatedRoom', () => { expect(federatedRoom.shouldUpdateRoomTopic('new topic')).to.be.equal(false); }); }); + + describe('#shouldUpdateMessage()', () => { + it('should return true if the old message is different from the new one', () => { + expect(MyClass.shouldUpdateMessage('new message', { msg: 'different' })).to.be.equal(true); + }); + + it('should return false if the old message is EQUAL from the new one', () => { + expect(MyClass.shouldUpdateMessage('new message', { msg: 'new message' })).to.be.equal(false); + }); + }); }); describe('FederatedRoom', () => { diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/Bridge.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/Bridge.spec.ts index d7b3ad11390..ee136e2ae27 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/Bridge.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/Bridge.spec.ts @@ -1,10 +1,16 @@ +/* eslint-disable import/first */ import { expect } from 'chai'; +import proxyquire from 'proxyquire'; -import { MatrixBridge } from '../../../../../../../../app/federation-v2/server/infrastructure/matrix/Bridge'; +const { MatrixBridge } = proxyquire.noCallThru().load('../../../../../../../../app/federation-v2/server/infrastructure/matrix/Bridge', { + 'meteor/fetch': { + '@global': true, + }, +}); describe('Federation - Infrastructure - Matrix - Bridge', () => { const defaultProxyDomain = 'server.com'; - const bridge = new MatrixBridge('', '', defaultProxyDomain, '', 3030, {} as any, () => {}); // eslint-disable-line + const bridge = new MatrixBridge('', '', defaultProxyDomain, '', 3030, {} as any, () => { }); // eslint-disable-line describe('#isUserIdFromTheSameHomeserver()', () => { it('should return true if the userId is from the same homeserver', () => { diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts index 08759444391..89903106e9a 100644 --- a/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/converters/RoomReceiver.spec.ts @@ -9,6 +9,7 @@ import { FederationRoomChangeTopicDto, FederationRoomChangeNameDto, FederationRoomChangeJoinRulesDto, + FederationRoomRedactEventDto, } from '../../../../../../../../../app/federation-v2/server/application/input/RoomReceiverDto'; import { MatrixEventType } from '../../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType'; import { EVENT_ORIGIN } from '../../../../../../../../../app/federation-v2/server/domain/IFederationBridge'; @@ -19,6 +20,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toRoomCreateDto()', () => { const event = { content: { was_internally_programatically_created: true, name: 'roomName', internalRoomId: 'internalRoomId' }, + event_id: 'eventId', room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', }; @@ -72,6 +74,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toRoomCreateDto(event as any); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', externalInviterId: '@marcos.defendi:matrix.org', @@ -86,6 +89,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toChangeRoomMembershipDto()', () => { const event = { + event_id: 'eventId', content: { name: 'roomName' }, room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', @@ -195,6 +199,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toChangeRoomMembershipDto(event as any, 'domain'); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', externalInviterId: '@marcos.defendi:matrix.org', @@ -213,29 +218,33 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toSendRoomMessageDto()', () => { const event = { + event_id: 'eventId', content: { body: 'msg' }, room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', }; it('should return an instance of FederationRoomReceiveExternalMessageDto', () => { - expect(MatrixRoomReceiverConverter.toSendRoomMessageDto({} as any)).to.be.instanceOf(FederationRoomReceiveExternalMessageDto); + expect(MatrixRoomReceiverConverter.toSendRoomMessageDto({ content: {} } as any)).to.be.instanceOf( + FederationRoomReceiveExternalMessageDto, + ); }); it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { - const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ room_id: event.room_id } as any); + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ room_id: event.room_id, content: {} } as any); expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); expect(result.normalizedRoomId).to.be.equal('roomId'); }); it('should convert the sender id to the a rc-format like (without any @ in it)', () => { - const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ sender: event.sender } as any); + const result = MatrixRoomReceiverConverter.toSendRoomMessageDto({ sender: event.sender, content: {} } as any); expect(result.normalizedSenderId).to.be.equal('marcos.defendi:matrix.org'); }); it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toSendRoomMessageDto(event as any); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', externalSenderId: '@marcos.defendi:matrix.org', @@ -247,6 +256,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toRoomChangeJoinRulesDto()', () => { const event = { + event_id: 'eventId', content: { join_rule: MatrixRoomJoinRules.JOIN }, room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', @@ -275,6 +285,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toRoomChangeJoinRulesDto(event as any); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', roomType: RoomType.CHANNEL, @@ -284,6 +295,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toRoomChangeNameDto()', () => { const event = { + event_id: 'eventId', content: { name: '@roomName' }, room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', @@ -307,6 +319,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toRoomChangeNameDto(event as any); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', normalizedRoomName: 'roomName', @@ -317,6 +330,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( describe('#toRoomChangeTopicDto()', () => { const event = { + event_id: 'eventId', content: { topic: 'room topic' }, room_id: '!roomId:matrix.org', sender: '@marcos.defendi:matrix.org', @@ -335,6 +349,7 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( it('should convert the event properly', () => { const result = MatrixRoomReceiverConverter.toRoomChangeTopicDto(event as any); expect(result).to.be.eql({ + externalEventId: 'eventId', externalRoomId: '!roomId:matrix.org', normalizedRoomId: 'roomId', roomTopic: 'room topic', @@ -342,4 +357,87 @@ describe('Federation - Infrastructure - Matrix - MatrixRoomReceiverConverter', ( }); }); }); + + describe('#toSendRoomFileMessageDto()', () => { + const event = { + event_id: 'eventId', + content: { body: 'filename', url: 'url', info: { mimetype: 'mime', size: 12 } }, + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should throw an error if the url is not present in the file event', () => { + expect(() => MatrixRoomReceiverConverter.toSendRoomFileMessageDto({ content: {} } as any)).to.throw( + Error, + 'Missing url in the file message', + ); + }); + + it('should throw an error if the mimetype is not present in the file event', () => { + expect(() => MatrixRoomReceiverConverter.toSendRoomFileMessageDto({ content: { url: 'url' } } as any)).to.throw( + Error, + 'Missing mimetype in the file message', + ); + }); + + it('should throw an error if the size is not present in the file event', () => { + expect(() => + MatrixRoomReceiverConverter.toSendRoomFileMessageDto({ content: { url: 'url', info: { mimetype: 'mime' } } } as any), + ).to.throw(Error, 'Missing size in the file message'); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toSendRoomFileMessageDto({ room_id: event.room_id, content: event.content } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toSendRoomFileMessageDto(event as any); + expect(result).to.be.eql({ + externalEventId: 'eventId', + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + externalSenderId: '@marcos.defendi:matrix.org', + normalizedSenderId: 'marcos.defendi:matrix.org', + messageBody: { + filename: event.content.body, + url: event.content.url, + mimetype: event.content.info.mimetype, + size: event.content.info.size, + messageText: event.content.body, + }, + }); + }); + }); + + describe('#toRoomRedactEventDto()', () => { + const event = { + event_id: 'eventId', + redacts: '$eventId', + room_id: '!roomId:matrix.org', + sender: '@marcos.defendi:matrix.org', + }; + + it('should return an instance of FederationRoomRedactEventDto', () => { + expect(MatrixRoomReceiverConverter.toRoomRedactEventDto({} as any)).to.be.instanceOf(FederationRoomRedactEventDto); + }); + + it('should return the basic room properties correctly (normalizedRoomId without any "!" and only the part before the ":") if any', () => { + const result = MatrixRoomReceiverConverter.toRoomRedactEventDto({ room_id: event.room_id } as any); + expect(result.externalRoomId).to.be.equal('!roomId:matrix.org'); + expect(result.normalizedRoomId).to.be.equal('roomId'); + }); + + it('should convert the event properly', () => { + const result = MatrixRoomReceiverConverter.toRoomRedactEventDto(event as any); + expect(result).to.be.eql({ + externalEventId: 'eventId', + externalRoomId: '!roomId:matrix.org', + normalizedRoomId: 'roomId', + redactsEvent: '$eventId', + externalSenderId: '@marcos.defendi:matrix.org', + }); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/handlers/Room.spec.ts b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/handlers/Room.spec.ts new file mode 100644 index 00000000000..8beab164e20 --- /dev/null +++ b/apps/meteor/tests/unit/app/federation-v2/server/unit/infrastructure/matrix/handlers/Room.spec.ts @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { + MatrixEnumRelatesToRelType, + MatrixEnumSendMessageType, +} from '../../../../../../../../../app/federation-v2/server/infrastructure/matrix/definitions/events/RoomMessageSent'; +import { MatrixRoomMessageSentHandler } from '../../../../../../../../../app/federation-v2/server/infrastructure/matrix/handlers/Room'; + +describe('Federation - Infrastructure - handlers - Room - MatrixRoomMessageSentHandler ', () => { + const normalMessageStub = sinon.stub(); + const editedMessageStub = sinon.stub(); + const fileMessageStub = sinon.stub(); + const roomService = { + onExternalMessageReceived: normalMessageStub, + onExternalFileMessageReceived: fileMessageStub, + onExternalMessageEditedReceived: editedMessageStub, + }; + const handler = new MatrixRoomMessageSentHandler(roomService as any); + + describe('#handle()', () => { + const handlers: Record<string, any> = { + [MatrixEnumSendMessageType.TEXT]: normalMessageStub, + [MatrixEnumSendMessageType.AUDIO]: fileMessageStub, + [MatrixEnumSendMessageType.FILE]: fileMessageStub, + [MatrixEnumSendMessageType.IMAGE]: fileMessageStub, + [MatrixEnumSendMessageType.NOTICE]: normalMessageStub, + [MatrixEnumSendMessageType.VIDEO]: fileMessageStub, + [MatrixEnumSendMessageType.EMOTE]: normalMessageStub, + }; + + Object.keys(handlers).forEach((type) => { + it(`should call the correct handler for ${type}`, async () => { + await handler.handle({ content: { msgtype: type, url: 'url', info: { mimetype: 'mime', size: 12 } } } as any); + expect(handlers[type].called).to.be.true; + }); + }); + + it('should call the default handler if no handler is found', async () => { + await handler.handle({ content: { msgtype: 'unknown', url: 'url', info: { mimetype: 'mime', size: 12 } } } as any); + expect(normalMessageStub.called).to.be.true; + }); + + it('should call the edit message method when it is an edition event', async () => { + await handler.handle({ + content: { + 'msgtype': MatrixEnumSendMessageType.TEXT, + 'm.new_content': {}, + 'm.relates_to': { rel_type: MatrixEnumRelatesToRelType.REPLACE }, + }, + } as any); + expect(editedMessageStub.called).to.be.true; + }); + }); +}); diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index f207f3b67a1..1c856816464 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -163,7 +163,7 @@ export interface IMessage extends IRocketChatRecord { attachments?: MessageAttachment[]; reactions?: { - [key: string]: { names?: (string | undefined)[]; usernames: string[] }; + [key: string]: { names?: (string | undefined)[]; usernames: string[]; federationReactionEventIds?: Record<string, string> }; }; private?: boolean; @@ -181,6 +181,9 @@ export interface IMessage extends IRocketChatRecord { html?: string; // Messages sent from visitors have this field token?: string; + federation?: { + eventId: string; + }; } export type MessageSystem = { @@ -193,6 +196,10 @@ export interface IEditedMessage extends IMessage { } export const isEditedMessage = (message: IMessage): message is IEditedMessage => 'editedAt' in message && 'editedBy' in message; +export const isDeletedMessage = (message: IMessage): message is IEditedMessage => + 'editedAt' in message && 'editedBy' in message && message.t === 'rm'; +export const isMessageFromMatrixFederation = (message: IMessage): boolean => + 'federation' in message && Boolean(message.federation?.eventId); export interface ITranslatedMessage extends IMessage { translations: { [key: string]: string } & { original?: string }; diff --git a/packages/core-typings/src/IUpload.ts b/packages/core-typings/src/IUpload.ts index 46a003b8105..c8f29b55430 100644 --- a/packages/core-typings/src/IUpload.ts +++ b/packages/core-typings/src/IUpload.ts @@ -2,6 +2,7 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; export interface IUpload extends IRocketChatRecord { typeGroup?: string; + description?: string; type?: string; name: string; aliases?: string; @@ -12,4 +13,11 @@ export interface IUpload extends IRocketChatRecord { userId?: string; progress?: number; etag?: string; + size?: number; + identify?: { + size: { + width: number; + height: number; + }; + }; } diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 5bf296b5c0b..db453dbb4e7 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -55,4 +55,12 @@ export interface IMessagesModel extends IBaseModel<IMessage> { countByType(type: IMessage['t'], options: CountDocumentsOptions): Promise<number>; findPaginatedPinnedByRoom(roomId: IMessage['rid'], options: FindOptions<IMessage>): FindPaginated<FindCursor<IMessage>>; + + setFederationReactionEventId(username: string, _id: string, reaction: string, federationEventId: string): Promise<void>; + + unsetFederationReactionEventId(federationEventId: string, _id: string, reaction: string): Promise<void>; + + findOneByFederationIdAndUsernameOnReactions(federationEventId: string, username: string): Promise<IMessage | null>; + + findOneByFederationId(federationEventId: string): Promise<IMessage | null>; } diff --git a/yarn.lock b/yarn.lock index d2f3839e177..a18e05fa40d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6203,6 +6203,7 @@ __metadata: "@types/cssom": ^0.4.1 "@types/dompurify": ^2.3.3 "@types/ejson": ^2.2.0 + "@types/emojione": ^2.2.6 "@types/express": ^4.17.13 "@types/express-rate-limit": ^5.1.3 "@types/fibers": ^3.1.1 @@ -9738,6 +9739,13 @@ __metadata: languageName: node linkType: hard +"@types/emojione@npm:^2.2.6": + version: 2.2.6 + resolution: "@types/emojione@npm:2.2.6" + checksum: 5a049b57ea99eb88359b257256cdb74f048f8cfddc58a7ea9a50ba5d937f9414025c0616205b67f63512fcbbba44471901209d8f3c7fb5a9741fb6d3038eb864 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.0, @types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -24414,7 +24422,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 + checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d languageName: node linkType: hard -- GitLab From 8ba1cb806286e7d8fcc9625a085200446dd5e519 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Fri, 23 Sep 2022 19:57:35 -0300 Subject: [PATCH 074/107] Chore: Bump vm2 to 3.9.11 (#26940) --- apps/meteor/package.json | 2 +- yarn.lock | 1589 ++------------------------------------ 2 files changed, 71 insertions(+), 1520 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a5c8b9ef323..cd26881d8dc 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -377,7 +377,7 @@ "use-subscription": "~1.6.0", "use-sync-external-store": "^1.2.0", "uuid": "^8.3.2", - "vm2": "^3.9.10", + "vm2": "^3.9.11", "webdav": "^4.11.0", "xml-crypto": "^2.1.4", "xml-encryption": "2.0.0", diff --git a/yarn.lock b/yarn.lock index a18e05fa40d..fff445850b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,20 +53,13 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.19.1": +"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.18.8, @babel/compat-data@npm:^7.19.1": version: 7.19.1 resolution: "@babel/compat-data@npm:7.19.1" checksum: f985887ea08a140e4af87a94d3fb17af0345491eb97f5a85b1840255c2e2a97429f32a8fd12a7aae9218af5f1024f1eb12a5cd280d2d69b2337583c17ea506ba languageName: node linkType: hard -"@babel/compat-data@npm:^7.18.8": - version: 7.18.13 - resolution: "@babel/compat-data@npm:7.18.13" - checksum: 869a730dc3ec40d4d5141b832d50b16702a2ea7bf5b87dc2761e7dfaa8deeafa03b8809fc42ff713ac1d450748dcdb07e1eb21f4633e10b87fd47be0065573e6 - languageName: node - linkType: hard - "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -91,30 +84,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": - version: 7.18.9 - resolution: "@babel/core@npm:7.18.9" - dependencies: - "@ampproject/remapping": ^2.1.0 - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.18.13 - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-module-transforms": ^7.18.9 - "@babel/helpers": ^7.18.9 - "@babel/parser": ^7.18.13 - "@babel/template": ^7.18.10 - "@babel/traverse": ^7.18.13 - "@babel/types": ^7.18.13 - convert-source-map: ^1.7.0 - debug: ^4.1.0 - gensync: ^1.0.0-beta.2 - json5: ^2.2.1 - semver: ^6.3.0 - checksum: 64b9088b03fdf659b334864ef93bed85d60c17b27fcbd72970f8eb9e0d3266ffa5a1926960f648f2db36b0bafec615f947ea5117d200599a0661b9f0a9cdf323 - languageName: node - linkType: hard - -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.19.1": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.13, @babel/core@npm:^7.19.1, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": version: 7.19.1 resolution: "@babel/core@npm:7.19.1" dependencies: @@ -137,7 +107,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.18.13, @babel/core@npm:~7.18.13": +"@babel/core@npm:~7.18.13": version: 7.18.13 resolution: "@babel/core@npm:7.18.13" dependencies: @@ -174,18 +144,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.7.2": - version: 7.18.9 - resolution: "@babel/generator@npm:7.18.9" - dependencies: - "@babel/types": ^7.18.13 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: 1c271e0c6f33e59f7845d88a1b0b9b0dce88164e80dec9274a716efa54c260e405e9462b160843e73f45382bf5b24d8e160e0121207e480c29b30e2ed0eb16d4 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.18.13, @babel/generator@npm:^7.19.0": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.19.0, @babel/generator@npm:^7.7.2": version: 7.19.0 resolution: "@babel/generator@npm:7.19.0" dependencies: @@ -215,21 +174,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-compilation-targets@npm:7.18.9" - dependencies: - "@babel/compat-data": ^7.18.8 - "@babel/helper-validator-option": ^7.18.6 - browserslist: ^4.20.2 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 2a9d71e124e098a9f45de4527ddd1982349d231827d341e00da9dfb967e260ecc7662c8b62abee4a010fb34d5f07a8d2155c974e0bc1928144cee5644910621d - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.19.0, @babel/helper-compilation-targets@npm:^7.19.1": +"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.19.0, @babel/helper-compilation-targets@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-compilation-targets@npm:7.19.1" dependencies: @@ -243,24 +188,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.16.7, @babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-create-class-features-plugin@npm:7.18.6" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.6 - "@babel/helper-function-name": ^7.18.6 - "@babel/helper-member-expression-to-functions": ^7.18.6 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-replace-supers": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 4d6da441ce329867338825c044c143f0b273cbfc6a20b9099e824a46f916584f44eabab073f78f02047d86719913e8f1a8bd72f42099ebe52691c29fabb992e4 - languageName: node - linkType: hard - -"@babel/helper-create-class-features-plugin@npm:^7.19.0": +"@babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-create-class-features-plugin@npm:7.19.0" dependencies: @@ -277,19 +205,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.18.6" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - regexpu-core: ^5.1.0 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 2d76e660cbfd0bfcb01ca9f177f0e9091c871a6b99f68ece6bcf4ab4a9df073485bdc2d87ecdfbde44b7f3723b26d13085d0f92082adb3ae80d31b246099f10a - languageName: node - linkType: hard - -"@babel/helper-create-regexp-features-plugin@npm:^7.19.0": +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.19.0" dependencies: @@ -319,22 +235,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.3.2": - version: 0.3.2 - resolution: "@babel/helper-define-polyfill-provider@npm:0.3.2" - dependencies: - "@babel/helper-compilation-targets": ^7.17.7 - "@babel/helper-plugin-utils": ^7.16.7 - debug: ^4.1.1 - lodash.debounce: ^4.0.8 - resolve: ^1.14.2 - semver: ^6.1.2 - peerDependencies: - "@babel/core": ^7.4.0-0 - checksum: 8f693ab8e9d73873c2e547c7764c7d32d73c14f8dcefdd67fd3a038eb75527e2222aa53412ea673b9bfc01c32a8779a60e77a7381bbdd83452f05c9b7ef69c2c - languageName: node - linkType: hard - "@babel/helper-define-polyfill-provider@npm:^0.3.3": version: 0.3.3 resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3" @@ -351,7 +251,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.18.6, @babel/helper-environment-visitor@npm:^7.18.9": +"@babel/helper-environment-visitor@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-environment-visitor@npm:7.18.9" checksum: b25101f6162ddca2d12da73942c08ad203d7668e06663df685634a8fde54a98bc015f6f62938e8554457a592a024108d45b8f3e651fd6dcdb877275b73cc4420 @@ -367,17 +267,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.18.6, @babel/helper-function-name@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-function-name@npm:7.18.9" - dependencies: - "@babel/template": ^7.18.6 - "@babel/types": ^7.18.9 - checksum: d04c44e0272f887c0c868651be7fc3c5690531bea10936f00d4cca3f6d5db65e76dfb49e8d553c42ae1fe1eba61ccce9f3d93ba2df50a66408c8d4c3cc61cf0c - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.19.0": +"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-function-name@npm:7.19.0" dependencies: @@ -396,7 +286,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.18.6, @babel/helper-member-expression-to-functions@npm:^7.18.9": +"@babel/helper-member-expression-to-functions@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-member-expression-to-functions@npm:7.18.9" dependencies: @@ -414,23 +304,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-module-transforms@npm:7.18.9" - dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/helper-validator-identifier": ^7.18.6 - "@babel/template": ^7.18.6 - "@babel/traverse": ^7.18.9 - "@babel/types": ^7.18.9 - checksum: af08c60ea239ff3d40eda542fceaab69de17e713f131e80ead08c975ba7a47dd55d439cb48cfb14ae7ec96704a10c989ff5a5240e52a39101cb44a49467ce058 - languageName: node - linkType: hard - -"@babel/helper-module-transforms@npm:^7.19.0": +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.18.9, @babel/helper-module-transforms@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helper-module-transforms@npm:7.19.0" dependencies: @@ -462,14 +336,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.13.0, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.18.9 - resolution: "@babel/helper-plugin-utils@npm:7.18.9" - checksum: ebae876cd60f1fe238c7210986093845fa5c4cad5feeda843ea4d780bf068256717650376d3af2a5e760f2ed6a35c065ae144f99c47da3e54aa6cba99d8804e0 - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.19.0": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.13.0, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.19.0 resolution: "@babel/helper-plugin-utils@npm:7.19.0" checksum: eedc996c633c8c207921c26ec2989eae0976336ecd9b9f1ac526498f52b5d136f7cd03c32b6fdf8d46a426f907c142de28592f383c42e5fba1e904cbffa05345 @@ -563,18 +430,7 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helpers@npm:7.18.9" - dependencies: - "@babel/template": ^7.18.6 - "@babel/traverse": ^7.18.9 - "@babel/types": ^7.18.9 - checksum: d0bd8255d36bfc65dc52ce75f7fea778c70287da2d64981db4c84fbdf9581409ecbd6433deff1c81da3a5acf26d7e4c364b3a4445efacf88f4f48e77c5b34d8d - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.19.0": +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.18.9, @babel/helpers@npm:^7.19.0": version: 7.19.0 resolution: "@babel/helpers@npm:7.19.0" dependencies: @@ -596,16 +452,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.6": - version: 7.18.9 - resolution: "@babel/parser@npm:7.18.9" - bin: - parser: ./bin/babel-parser.js - checksum: 81a966b334e3ef397e883c64026265a5ae0ad435a86f52a84f60a5ee1efc0738c1f42c55e0dc5f191cc6a83ba0c61350433eee417bf1dff160ca5f3cfde244c6 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.1": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.13, @babel/parser@npm:^7.19.1": version: 7.19.1 resolution: "@babel/parser@npm:7.19.1" bin: @@ -614,15 +461,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.18.13, @babel/parser@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/parser@npm:7.19.0" - bin: - parser: ./bin/babel-parser.js - checksum: af86d829bfeb60e0dcf54a43489c2514674b6c8d9bb24cf112706772125752fcd517877ad30501d533fa85f70a439d02eebeec3be9c2e95499853367184e0da7 - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -647,20 +485,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-async-generator-functions@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.18.10" - dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-remap-async-to-generator": ^7.18.9 - "@babel/plugin-syntax-async-generators": ^7.8.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3a6c25085021053830f6c57780118d3337935ac3309eef7f09b11e413d189eed8119d50cbddeb4c8c02f42f8cc01e62a4667b869be6e158f40030bafb92a0629 - languageName: node - linkType: hard - "@babel/plugin-proposal-async-generator-functions@npm:^7.19.1": version: 7.19.1 resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.19.1" @@ -1132,18 +956,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.16.7, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.16.7 - resolution: "@babel/plugin-syntax-typescript@npm:7.16.7" - dependencies: - "@babel/helper-plugin-utils": ^7.16.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 661e636060609ede9a402e22603b01784c21fabb0a637e65f561c8159351fe0130bbc11fdefe31902107885e3332fc34d95eb652ac61d3f61f2d61f5da20609e - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.18.6": +"@babel/plugin-syntax-typescript@npm:^7.18.6, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.18.6 resolution: "@babel/plugin-syntax-typescript@npm:7.18.6" dependencies: @@ -1200,25 +1013,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-classes@npm:7.18.9" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.18.9 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-replace-supers": ^7.18.9 - "@babel/helper-split-export-declaration": ^7.18.6 - globals: ^11.1.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: d7e953c0cf32af64e75db1277d2556c04635f32691ef462436897840be6f8021d4f85ee96134cb796a12dda549cf53346fedf96b671885f881bc4037c9d120ad - languageName: node - linkType: hard - -"@babel/plugin-transform-classes@npm:^7.19.0": +"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.19.0": version: 7.19.0 resolution: "@babel/plugin-transform-classes@npm:7.19.0" dependencies: @@ -1248,18 +1043,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-destructuring@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1a9b85dff67fd248fa8a2488ef59df3eb4dd4ca6007ff7db9f780c7873630a13bc16cfb2ad8f4c4ca966e42978410d1e4b306545941fe62769f2683f34973acd - languageName: node - linkType: hard - -"@babel/plugin-transform-destructuring@npm:^7.18.13": +"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.18.13": version: 7.18.13 resolution: "@babel/plugin-transform-destructuring@npm:7.18.13" dependencies: @@ -1390,21 +1174,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.18.9" - dependencies: - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-module-transforms": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-validator-identifier": ^7.18.6 - babel-plugin-dynamic-import-node: ^2.3.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 6122d9901ed5dc56d9db843efc9249fe20d769a11989bbbf5a806ed4f086def949185198aa767888481babf70fc52b6b3e297a991e2b02b4f34ffb03d998d1e3 - languageName: node - linkType: hard - "@babel/plugin-transform-modules-systemjs@npm:^7.19.0": version: 7.19.0 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.19.0" @@ -1432,18 +1201,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.18.6" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 6ef64aa3dad68df139eeaa7b6e9bb626be8f738ed5ed4db765d516944b1456d513b6bad3bb60fff22babe73de26436fd814a4228705b2d3d2fdb272c31da35e2 - languageName: node - linkType: hard - "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.19.1": version: 7.19.1 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.19.1" @@ -1584,19 +1341,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.12.1, @babel/plugin-transform-spread@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-spread@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-skip-transparent-expression-wrappers": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 59489dd6212bd21debdf77746d9fa02dfe36f7062dc08742b8841d04312a26ea37bc0d71c71a6e37c3ab81dce744faa7f23fa94b0915593458f6adc35c087766 - languageName: node - linkType: hard - -"@babel/plugin-transform-spread@npm:^7.19.0": +"@babel/plugin-transform-spread@npm:^7.12.1, @babel/plugin-transform-spread@npm:^7.19.0": version: 7.19.0 resolution: "@babel/plugin-transform-spread@npm:7.19.0" dependencies: @@ -1641,19 +1386,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.16.7": - version: 7.16.8 - resolution: "@babel/plugin-transform-typescript@npm:7.16.8" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.16.7 - "@babel/helper-plugin-utils": ^7.16.7 - "@babel/plugin-syntax-typescript": ^7.16.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a76d0afcbd550208cf2e7cdedb4f2d3ca3fa287640a4858a5ee0a28270b784d7d20d5a51b5997dc84514e066a5ebef9e0a0f74ed9fffae09e73984786dd08036 - languageName: node - linkType: hard - "@babel/plugin-transform-typescript@npm:^7.18.6": version: 7.19.1 resolution: "@babel/plugin-transform-typescript@npm:7.19.1" @@ -1690,177 +1422,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11": - version: 7.18.9 - resolution: "@babel/preset-env@npm:7.18.9" - dependencies: - "@babel/compat-data": ^7.18.8 - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-validator-option": ^7.18.6 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-async-generator-functions": ^7.18.10 - "@babel/plugin-proposal-class-properties": ^7.18.6 - "@babel/plugin-proposal-class-static-block": ^7.18.6 - "@babel/plugin-proposal-dynamic-import": ^7.18.6 - "@babel/plugin-proposal-export-namespace-from": ^7.18.9 - "@babel/plugin-proposal-json-strings": ^7.18.6 - "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 - "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 - "@babel/plugin-proposal-numeric-separator": ^7.18.6 - "@babel/plugin-proposal-object-rest-spread": ^7.18.9 - "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 - "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-private-methods": ^7.18.6 - "@babel/plugin-proposal-private-property-in-object": ^7.18.6 - "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.18.6 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-transform-arrow-functions": ^7.18.6 - "@babel/plugin-transform-async-to-generator": ^7.18.6 - "@babel/plugin-transform-block-scoped-functions": ^7.18.6 - "@babel/plugin-transform-block-scoping": ^7.18.9 - "@babel/plugin-transform-classes": ^7.18.9 - "@babel/plugin-transform-computed-properties": ^7.18.9 - "@babel/plugin-transform-destructuring": ^7.18.9 - "@babel/plugin-transform-dotall-regex": ^7.18.6 - "@babel/plugin-transform-duplicate-keys": ^7.18.9 - "@babel/plugin-transform-exponentiation-operator": ^7.18.6 - "@babel/plugin-transform-for-of": ^7.18.8 - "@babel/plugin-transform-function-name": ^7.18.9 - "@babel/plugin-transform-literals": ^7.18.9 - "@babel/plugin-transform-member-expression-literals": ^7.18.6 - "@babel/plugin-transform-modules-amd": ^7.18.6 - "@babel/plugin-transform-modules-commonjs": ^7.18.6 - "@babel/plugin-transform-modules-systemjs": ^7.18.9 - "@babel/plugin-transform-modules-umd": ^7.18.6 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.18.6 - "@babel/plugin-transform-new-target": ^7.18.6 - "@babel/plugin-transform-object-super": ^7.18.6 - "@babel/plugin-transform-parameters": ^7.18.8 - "@babel/plugin-transform-property-literals": ^7.18.6 - "@babel/plugin-transform-regenerator": ^7.18.6 - "@babel/plugin-transform-reserved-words": ^7.18.6 - "@babel/plugin-transform-shorthand-properties": ^7.18.6 - "@babel/plugin-transform-spread": ^7.18.9 - "@babel/plugin-transform-sticky-regex": ^7.18.6 - "@babel/plugin-transform-template-literals": ^7.18.9 - "@babel/plugin-transform-typeof-symbol": ^7.18.9 - "@babel/plugin-transform-unicode-escapes": ^7.18.10 - "@babel/plugin-transform-unicode-regex": ^7.18.6 - "@babel/preset-modules": ^0.1.5 - "@babel/types": ^7.18.10 - babel-plugin-polyfill-corejs2: ^0.3.2 - babel-plugin-polyfill-corejs3: ^0.5.3 - babel-plugin-polyfill-regenerator: ^0.4.0 - core-js-compat: ^3.22.1 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 311002b9255d1aa261afe712ab73a93687652437804e2f44e6cc55438f8b199463f53bb2b8e0912b0034f208a42eee664a9e126a6061ca504a792ede97dd027e - languageName: node - linkType: hard - -"@babel/preset-env@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/preset-env@npm:7.18.10" - dependencies: - "@babel/compat-data": ^7.18.8 - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-validator-option": ^7.18.6 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-async-generator-functions": ^7.18.10 - "@babel/plugin-proposal-class-properties": ^7.18.6 - "@babel/plugin-proposal-class-static-block": ^7.18.6 - "@babel/plugin-proposal-dynamic-import": ^7.18.6 - "@babel/plugin-proposal-export-namespace-from": ^7.18.9 - "@babel/plugin-proposal-json-strings": ^7.18.6 - "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 - "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 - "@babel/plugin-proposal-numeric-separator": ^7.18.6 - "@babel/plugin-proposal-object-rest-spread": ^7.18.9 - "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 - "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-private-methods": ^7.18.6 - "@babel/plugin-proposal-private-property-in-object": ^7.18.6 - "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.18.6 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-transform-arrow-functions": ^7.18.6 - "@babel/plugin-transform-async-to-generator": ^7.18.6 - "@babel/plugin-transform-block-scoped-functions": ^7.18.6 - "@babel/plugin-transform-block-scoping": ^7.18.9 - "@babel/plugin-transform-classes": ^7.18.9 - "@babel/plugin-transform-computed-properties": ^7.18.9 - "@babel/plugin-transform-destructuring": ^7.18.9 - "@babel/plugin-transform-dotall-regex": ^7.18.6 - "@babel/plugin-transform-duplicate-keys": ^7.18.9 - "@babel/plugin-transform-exponentiation-operator": ^7.18.6 - "@babel/plugin-transform-for-of": ^7.18.8 - "@babel/plugin-transform-function-name": ^7.18.9 - "@babel/plugin-transform-literals": ^7.18.9 - "@babel/plugin-transform-member-expression-literals": ^7.18.6 - "@babel/plugin-transform-modules-amd": ^7.18.6 - "@babel/plugin-transform-modules-commonjs": ^7.18.6 - "@babel/plugin-transform-modules-systemjs": ^7.18.9 - "@babel/plugin-transform-modules-umd": ^7.18.6 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.18.6 - "@babel/plugin-transform-new-target": ^7.18.6 - "@babel/plugin-transform-object-super": ^7.18.6 - "@babel/plugin-transform-parameters": ^7.18.8 - "@babel/plugin-transform-property-literals": ^7.18.6 - "@babel/plugin-transform-regenerator": ^7.18.6 - "@babel/plugin-transform-reserved-words": ^7.18.6 - "@babel/plugin-transform-shorthand-properties": ^7.18.6 - "@babel/plugin-transform-spread": ^7.18.9 - "@babel/plugin-transform-sticky-regex": ^7.18.6 - "@babel/plugin-transform-template-literals": ^7.18.9 - "@babel/plugin-transform-typeof-symbol": ^7.18.9 - "@babel/plugin-transform-unicode-escapes": ^7.18.10 - "@babel/plugin-transform-unicode-regex": ^7.18.6 - "@babel/preset-modules": ^0.1.5 - "@babel/types": ^7.18.10 - babel-plugin-polyfill-corejs2: ^0.3.2 - babel-plugin-polyfill-corejs3: ^0.5.3 - babel-plugin-polyfill-regenerator: ^0.4.0 - core-js-compat: ^3.22.1 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 36eeb7157021091c8047703833b7a28e4963865d16968a5b9dbffe1eb05e44307a8d29ad45d81fd23817f68290b52921c42f513a93996c7083d23d5e2cea0c6b - languageName: node - linkType: hard - -"@babel/preset-env@npm:^7.19.1": +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.18.10, @babel/preset-env@npm:^7.19.1": version: 7.19.1 resolution: "@babel/preset-env@npm:7.19.1" dependencies: @@ -1989,20 +1551,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.12.7": - version: 7.16.7 - resolution: "@babel/preset-typescript@npm:7.16.7" - dependencies: - "@babel/helper-plugin-utils": ^7.16.7 - "@babel/helper-validator-option": ^7.16.7 - "@babel/plugin-transform-typescript": ^7.16.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 44e2f3fa302befe0dc50a01b79e5aa8c27a9c7047c46df665beae97201173030646ddf7c83d7d3ed3724fc38151745b11693e7b4502c81c4cd67781ff5677da5 - languageName: node - linkType: hard - -"@babel/preset-typescript@npm:^7.18.6": +"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:^7.18.6": version: 7.18.6 resolution: "@babel/preset-typescript@npm:7.18.6" dependencies: @@ -2039,16 +1588,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.18.9 - resolution: "@babel/runtime@npm:7.18.9" - dependencies: - regenerator-runtime: ^0.13.4 - checksum: 36dd736baba7164e82b3cc9d43e081f0cb2d05ff867ad39cac515d99546cee75b7f782018b02a3dcf5f2ef3d27f319faa68965fdfec49d4912c60c6002353a2e - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.6.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.19.0 resolution: "@babel/runtime@npm:7.19.0" dependencies: @@ -2066,18 +1606,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @babel/template@npm:^7.18.6, @babel/template@npm:^7.3.3": - version: 7.18.6 - resolution: "@babel/template@npm:7.18.6" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/parser": ^7.18.6 - "@babel/types": ^7.18.6 - checksum: cb02ed804b7b1938dbecef4e01562013b80681843dd391933315b3dd9880820def3b5b1bff6320d6e4c6a1d63d1d5799630d658ec6b0369c5505e7e4029c38fb - languageName: node - linkType: hard - -"@babel/template@npm:^7.18.10": +"@babel/template@npm:^7.12.7, @babel/template@npm:^7.18.10, @babel/template@npm:^7.3.3": version: 7.18.10 resolution: "@babel/template@npm:7.18.10" dependencies: @@ -2088,43 +1617,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.7.2": - version: 7.18.9 - resolution: "@babel/traverse@npm:7.18.9" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.18.13 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.18.9 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.18.13 - "@babel/types": ^7.18.13 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: 0445a51952ea1664a5719d9b1f8bf04be6f1933bcf54915fecc544c844a5dad2ac56f3b555723bbf741ef680d7fd64f6a5d69cfd08d518a4089c79a734270162 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.18.13": - version: 7.19.0 - resolution: "@babel/traverse@npm:7.19.0" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.19.0 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.19.0 - "@babel/types": ^7.19.0 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: dcbd1316c9f4bf3cefee45b6f5194590563aa5d123500a60d3c8d714bef279205014c8e599ebafc469967199a7622e1444cd0235c16d4243da437e3f1281771e - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.18.13, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.7.2": version: 7.19.1 resolution: "@babel/traverse@npm:7.19.1" dependencies: @@ -2142,18 +1635,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.18.9 - resolution: "@babel/types@npm:7.18.9" - dependencies: - "@babel/helper-string-parser": ^7.18.10 - "@babel/helper-validator-identifier": ^7.18.6 - to-fast-properties: ^2.0.0 - checksum: f0e0147267895fd8a5b82133e711ce7ce99941f3ce63647e0e3b00656a7afe48a8aa48edbae27543b701794d2b29a562a08f51f88f41df401abce7c3acc5e13a - languageName: node - linkType: hard - -"@babel/types@npm:^7.18.10, @babel/types@npm:^7.18.13, @babel/types@npm:^7.19.0": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.19.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.19.0 resolution: "@babel/types@npm:7.19.0" dependencies: @@ -2495,24 +1977,7 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.3.0": - version: 1.3.0 - resolution: "@eslint/eslintrc@npm:1.3.0" - dependencies: - ajv: ^6.12.4 - debug: ^4.3.2 - espree: ^9.3.2 - globals: ^13.15.0 - ignore: ^5.2.0 - import-fresh: ^3.2.1 - js-yaml: ^4.1.0 - minimatch: ^3.1.2 - strip-json-comments: ^3.1.1 - checksum: a1e734ad31a8b5328dce9f479f185fd4fc83dd7f06c538e1fa457fd8226b89602a55cc6458cd52b29573b01cdfaf42331be8cfc1fec732570086b591f4ed6515 - languageName: node - linkType: hard - -"@eslint/eslintrc@npm:^1.3.2": +"@eslint/eslintrc@npm:^1.3.0, @eslint/eslintrc@npm:^1.3.2": version: 1.3.2 resolution: "@eslint/eslintrc@npm:1.3.2" dependencies: @@ -3276,17 +2741,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.0, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.13 - resolution: "@jridgewell/trace-mapping@npm:0.3.13" - dependencies: - "@jridgewell/resolve-uri": ^3.0.3 - "@jridgewell/sourcemap-codec": ^1.4.10 - checksum: e38254e830472248ca10a6ed1ae75af5e8514f0680245a5e7b53bc3c030fd8691d4d3115d80595b45d3badead68269769ed47ecbbdd67db1343a11f05700e75a - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.15": +"@jridgewell/trace-mapping@npm:^0.3.0, @jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.15 resolution: "@jridgewell/trace-mapping@npm:0.3.15" dependencies: @@ -6433,7 +5888,7 @@ __metadata: use-subscription: ~1.6.0 use-sync-external-store: ^1.2.0 uuid: ^8.3.2 - vm2: ^3.9.10 + vm2: ^3.9.11 webdav: ^4.11.0 webpack: ^4.46.0 xml-crypto: ^2.1.4 @@ -6965,42 +6420,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:6.5.10, @storybook/addon-actions@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-actions@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - fast-deep-equal: ^3.1.3 - global: ^4.4.0 - lodash: ^4.17.21 - polished: ^4.2.2 - prop-types: ^15.7.2 - react-inspector: ^5.1.0 - regenerator-runtime: ^0.13.7 - telejson: ^6.0.8 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - uuid-browser: ^3.1.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: b864ceb0ec9aef76c438cfd55977946619954e07b2b822205e5209e3901cc9ae669babc9304026e48e3717e075212c9e5175d62fd63183cf696e3e196f1f6dd8 - languageName: node - linkType: hard - -"@storybook/addon-actions@npm:6.5.12, @storybook/addon-actions@npm:~6.5.12": +"@storybook/addon-actions@npm:6.5.12, @storybook/addon-actions@npm:~6.5.10, @storybook/addon-actions@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/addon-actions@npm:6.5.12" dependencies: @@ -7035,36 +6455,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:6.5.10, @storybook/addon-backgrounds@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-backgrounds@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - global: ^4.4.0 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 665ff48ea7fcea2fd126218a6253171f222cc15290f18c0b84b1f2b6adfc333328b79db762d404ff9caf449776162e66532a5a39626b28bd168abff3b58afdd2 - languageName: node - linkType: hard - -"@storybook/addon-backgrounds@npm:6.5.12": +"@storybook/addon-backgrounds@npm:6.5.12, @storybook/addon-backgrounds@npm:~6.5.10": version: 6.5.12 resolution: "@storybook/addon-backgrounds@npm:6.5.12" dependencies: @@ -7093,34 +6484,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-controls@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-controls@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/node-logger": 6.5.10 - "@storybook/store": 6.5.10 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - lodash: ^4.17.21 - ts-dedent: ^2.0.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 3c8152e4a4be960a7376ab1b1dc405fb3b6eeab367684766330cfb260519420f693d19b46225fd66976b4fa16e2e888585bfa571436507b2bf10f9905dfa968e - languageName: node - linkType: hard - "@storybook/addon-controls@npm:6.5.12": version: 6.5.12 resolution: "@storybook/addon-controls@npm:6.5.12" @@ -7149,54 +6512,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-docs@npm:6.5.10, @storybook/addon-docs@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-docs@npm:6.5.10" - dependencies: - "@babel/plugin-transform-react-jsx": ^7.12.12 - "@babel/preset-env": ^7.12.11 - "@jest/transform": ^26.6.2 - "@mdx-js/react": ^1.6.22 - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/docs-tools": 6.5.10 - "@storybook/mdx1-csf": ^0.0.1 - "@storybook/node-logger": 6.5.10 - "@storybook/postinstall": 6.5.10 - "@storybook/preview-web": 6.5.10 - "@storybook/source-loader": 6.5.10 - "@storybook/store": 6.5.10 - "@storybook/theming": 6.5.10 - babel-loader: ^8.0.0 - core-js: ^3.8.2 - fast-deep-equal: ^3.1.3 - global: ^4.4.0 - lodash: ^4.17.21 - regenerator-runtime: ^0.13.7 - remark-external-links: ^8.0.0 - remark-slug: ^6.0.0 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - peerDependencies: - "@storybook/mdx2-csf": ^0.0.3 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@storybook/mdx2-csf": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 5fecd18ea3ddbe820c23c06f34a75e2f448315ee08e6ea0ae548db4705a8148ec57804916b2c571556282e507dd543f4538b189d0366da73a592c58caa89d3ab - languageName: node - linkType: hard - -"@storybook/addon-docs@npm:6.5.12, @storybook/addon-docs@npm:~6.5.12": +"@storybook/addon-docs@npm:6.5.12, @storybook/addon-docs@npm:~6.5.10, @storybook/addon-docs@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/addon-docs@npm:6.5.12" dependencies: @@ -7243,67 +6559,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-essentials@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-essentials@npm:6.5.10" - dependencies: - "@storybook/addon-actions": 6.5.10 - "@storybook/addon-backgrounds": 6.5.10 - "@storybook/addon-controls": 6.5.10 - "@storybook/addon-docs": 6.5.10 - "@storybook/addon-measure": 6.5.10 - "@storybook/addon-outline": 6.5.10 - "@storybook/addon-toolbars": 6.5.10 - "@storybook/addon-viewport": 6.5.10 - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/node-logger": 6.5.10 - core-js: ^3.8.2 - regenerator-runtime: ^0.13.7 - ts-dedent: ^2.0.0 - peerDependencies: - "@babel/core": ^7.9.6 - peerDependenciesMeta: - "@storybook/angular": - optional: true - "@storybook/builder-manager4": - optional: true - "@storybook/builder-manager5": - optional: true - "@storybook/builder-webpack4": - optional: true - "@storybook/builder-webpack5": - optional: true - "@storybook/html": - optional: true - "@storybook/vue": - optional: true - "@storybook/vue3": - optional: true - "@storybook/web-components": - optional: true - lit: - optional: true - lit-html: - optional: true - react: - optional: true - react-dom: - optional: true - svelte: - optional: true - sveltedoc-parser: - optional: true - vue: - optional: true - webpack: - optional: true - checksum: 968286922924840bd00221d17e0499b98c153677ea9e220e07ab2e34d17d76670d4549dbb517cc35326b890723cc08d7b138a22662aa508e51d864e1f7b6975b - languageName: node - linkType: hard - -"@storybook/addon-essentials@npm:~6.5.12": +"@storybook/addon-essentials@npm:~6.5.10, @storybook/addon-essentials@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/addon-essentials@npm:6.5.12" dependencies: @@ -7454,30 +6710,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-measure@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-measure@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - core-js: ^3.8.2 - global: ^4.4.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 7a6be7fc80be358c329694ab5eb75a027210afaa8185c04774c741fdca4871b90937d46d3cd16f66d195dd78bb20d3f8734f3aa0863636119895f9c6253e834a - languageName: node - linkType: hard - "@storybook/addon-measure@npm:6.5.12": version: 6.5.12 resolution: "@storybook/addon-measure@npm:6.5.12" @@ -7502,32 +6734,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-outline@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-outline@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - core-js: ^3.8.2 - global: ^4.4.0 - regenerator-runtime: ^0.13.7 - ts-dedent: ^2.0.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 8d3e12a612fd51b3b8c49f6ff6ac145f510cfba85b00e08b0df625b99f9677c0532060bd8a132ea70e8052d9c09847bdba27caa1b69e51dd6d7845d38621dccf - languageName: node - linkType: hard - "@storybook/addon-outline@npm:6.5.12": version: 6.5.12 resolution: "@storybook/addon-outline@npm:6.5.12" @@ -7567,29 +6773,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-toolbars@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-toolbars@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 49c44596fdee713703ed69c47895a21151892d48af27d859f0c8c1b8be0b08be7e4945fadcb9053f6025c29dba93f0d5cd8ba34f090bd025e0d9ef5859e5bc75 - languageName: node - linkType: hard - "@storybook/addon-toolbars@npm:6.5.12": version: 6.5.12 resolution: "@storybook/addon-toolbars@npm:6.5.12" @@ -7613,34 +6796,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-viewport@npm:6.5.10, @storybook/addon-viewport@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/addon-viewport@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/theming": 6.5.10 - core-js: ^3.8.2 - global: ^4.4.0 - memoizerific: ^1.11.3 - prop-types: ^15.7.2 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 6cbd32053d2b4947942b0bab0ab016817988192d52361b4f2e08420f6d94128174974a0ec9b7ee4167de8f7cb91b3a3a8c8336a398a21cb567ba633efbf9e2cf - languageName: node - linkType: hard - -"@storybook/addon-viewport@npm:6.5.12": +"@storybook/addon-viewport@npm:6.5.12, @storybook/addon-viewport@npm:~6.5.10": version: 6.5.12 resolution: "@storybook/addon-viewport@npm:6.5.12" dependencies: @@ -7667,7 +6823,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.10, @storybook/addons@npm:~6.5.10": +"@storybook/addons@npm:6.5.10": version: 6.5.10 resolution: "@storybook/addons@npm:6.5.10" dependencies: @@ -7689,7 +6845,7 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.12": +"@storybook/addons@npm:6.5.12, @storybook/addons@npm:~6.5.10": version: 6.5.12 resolution: "@storybook/addons@npm:6.5.12" dependencies: @@ -7767,68 +6923,7 @@ __metadata: languageName: node linkType: hard -"@storybook/builder-webpack4@npm:6.5.10, @storybook/builder-webpack4@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/builder-webpack4@npm:6.5.10" - dependencies: - "@babel/core": ^7.12.10 - "@storybook/addons": 6.5.10 - "@storybook/api": 6.5.10 - "@storybook/channel-postmessage": 6.5.10 - "@storybook/channels": 6.5.10 - "@storybook/client-api": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/components": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/node-logger": 6.5.10 - "@storybook/preview-web": 6.5.10 - "@storybook/router": 6.5.10 - "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.10 - "@storybook/theming": 6.5.10 - "@storybook/ui": 6.5.10 - "@types/node": ^14.0.10 || ^16.0.0 - "@types/webpack": ^4.41.26 - autoprefixer: ^9.8.6 - babel-loader: ^8.0.0 - case-sensitive-paths-webpack-plugin: ^2.3.0 - core-js: ^3.8.2 - css-loader: ^3.6.0 - file-loader: ^6.2.0 - find-up: ^5.0.0 - fork-ts-checker-webpack-plugin: ^4.1.6 - glob: ^7.1.6 - glob-promise: ^3.4.0 - global: ^4.4.0 - html-webpack-plugin: ^4.0.0 - pnp-webpack-plugin: 1.6.4 - postcss: ^7.0.36 - postcss-flexbugs-fixes: ^4.2.1 - postcss-loader: ^4.2.0 - raw-loader: ^4.0.2 - stable: ^0.1.8 - style-loader: ^1.3.0 - terser-webpack-plugin: ^4.2.3 - ts-dedent: ^2.0.0 - url-loader: ^4.1.1 - util-deprecate: ^1.0.2 - webpack: 4 - webpack-dev-middleware: ^3.7.3 - webpack-filter-warnings-plugin: ^1.2.1 - webpack-hot-middleware: ^2.25.1 - webpack-virtual-modules: ^0.2.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 26921bbc477b8cc69a9515996f4e4a4b79ba43f783dab96930067c48ca6d127397ab7a461c25e3120468b99cad1cb641fbb85a0cd6ecf25661e2da2c182a97e6 - languageName: node - linkType: hard - -"@storybook/builder-webpack4@npm:6.5.12, @storybook/builder-webpack4@npm:~6.5.12": +"@storybook/builder-webpack4@npm:6.5.12, @storybook/builder-webpack4@npm:~6.5.10, @storybook/builder-webpack4@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/builder-webpack4@npm:6.5.12" dependencies: @@ -8091,7 +7186,7 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.12": +"@storybook/client-logger@npm:6.5.12, @storybook/client-logger@npm:^6.4.0": version: 6.5.12 resolution: "@storybook/client-logger@npm:6.5.12" dependencies: @@ -8101,16 +7196,6 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:^6.4.0": - version: 6.5.9 - resolution: "@storybook/client-logger@npm:6.5.9" - dependencies: - core-js: ^3.8.2 - global: ^4.4.0 - checksum: 5b72d93a57fae8d188bb40db0a3af3ce9f3ccc58751e90d38e0786b58f26a5358d10339916455646a8d60e2cc749d761990927fdeb06e5f09e68d48fe50a5de7 - languageName: node - linkType: hard - "@storybook/components@npm:6.5.10": version: 6.5.10 resolution: "@storybook/components@npm:6.5.10" @@ -8365,69 +7450,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-server@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/core-server@npm:6.5.10" - dependencies: - "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-webpack4": 6.5.10 - "@storybook/core-client": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/core-events": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/csf-tools": 6.5.10 - "@storybook/manager-webpack4": 6.5.10 - "@storybook/node-logger": 6.5.10 - "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.10 - "@storybook/telemetry": 6.5.10 - "@types/node": ^14.0.10 || ^16.0.0 - "@types/node-fetch": ^2.5.7 - "@types/pretty-hrtime": ^1.0.0 - "@types/webpack": ^4.41.26 - better-opn: ^2.1.1 - boxen: ^5.1.2 - chalk: ^4.1.0 - cli-table3: ^0.6.1 - commander: ^6.2.1 - compression: ^1.7.4 - core-js: ^3.8.2 - cpy: ^8.1.2 - detect-port: ^1.3.0 - express: ^4.17.1 - fs-extra: ^9.0.1 - global: ^4.4.0 - globby: ^11.0.2 - ip: ^2.0.0 - lodash: ^4.17.21 - node-fetch: ^2.6.7 - open: ^8.4.0 - pretty-hrtime: ^1.0.3 - prompts: ^2.4.0 - regenerator-runtime: ^0.13.7 - serve-favicon: ^2.5.0 - slash: ^3.0.0 - telejson: ^6.0.8 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - watchpack: ^2.2.0 - webpack: 4 - ws: ^8.2.3 - x-default-browser: ^0.4.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@storybook/builder-webpack5": - optional: true - "@storybook/manager-webpack5": - optional: true - typescript: - optional: true - checksum: 0359f8cf68e2a207d07ec631d0615c30991c78bcbe3ebe50cb8df8dd5159ab939d52789b84a50e074c027c253f74f813f745b4a002a5cf945de50a0069e0e758 - languageName: node - linkType: hard - "@storybook/core-server@npm:6.5.12": version: 6.5.12 resolution: "@storybook/core-server@npm:6.5.12" @@ -8491,27 +7513,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/core@npm:6.5.10" - dependencies: - "@storybook/core-client": 6.5.10 - "@storybook/core-server": 6.5.10 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - webpack: "*" - peerDependenciesMeta: - "@storybook/builder-webpack5": - optional: true - "@storybook/manager-webpack5": - optional: true - typescript: - optional: true - checksum: ee80fa596cfc305138089757b1f095a0b44ed403ff1db727e99190a5d04cca84614faa816ed881b60c7ed91a4d268ee91632cb7bcaa7f2a2127424acd66a2c96 - languageName: node - linkType: hard - "@storybook/core@npm:6.5.12": version: 6.5.12 resolution: "@storybook/core@npm:6.5.12" @@ -8533,33 +7534,6 @@ __metadata: languageName: node linkType: hard -"@storybook/csf-tools@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/csf-tools@npm:6.5.10" - dependencies: - "@babel/core": ^7.12.10 - "@babel/generator": ^7.12.11 - "@babel/parser": ^7.12.11 - "@babel/plugin-transform-react-jsx": ^7.12.12 - "@babel/preset-env": ^7.12.11 - "@babel/traverse": ^7.12.11 - "@babel/types": ^7.12.11 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/mdx1-csf": ^0.0.1 - core-js: ^3.8.2 - fs-extra: ^9.0.1 - global: ^4.4.0 - regenerator-runtime: ^0.13.7 - ts-dedent: ^2.0.0 - peerDependencies: - "@storybook/mdx2-csf": ^0.0.3 - peerDependenciesMeta: - "@storybook/mdx2-csf": - optional: true - checksum: 9bb4b61822760520c91da78b734a05c1f5145ad2e91f73cfe03aa900a6f40fd455c1fc2c3b1529a97a5e33246efb44462c68ad72b8dbf8f0b1811b7491411267 - languageName: node - linkType: hard - "@storybook/csf-tools@npm:6.5.12": version: 6.5.12 resolution: "@storybook/csf-tools@npm:6.5.12" @@ -8605,21 +7579,6 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-tools@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/docs-tools@npm:6.5.10" - dependencies: - "@babel/core": ^7.12.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.10 - core-js: ^3.8.2 - doctrine: ^3.0.0 - lodash: ^4.17.21 - regenerator-runtime: ^0.13.7 - checksum: 7fe14992ba94c31879964001a192f338bd53399a9582c598ab1681a05efb30999059329b1f7a4cbd33947778f17e6929d0f983cf54c36cb9e371414044f5dd89 - languageName: node - linkType: hard - "@storybook/docs-tools@npm:6.5.12": version: 6.5.12 resolution: "@storybook/docs-tools@npm:6.5.12" @@ -8648,56 +7607,7 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-webpack4@npm:6.5.10, @storybook/manager-webpack4@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/manager-webpack4@npm:6.5.10" - dependencies: - "@babel/core": ^7.12.10 - "@babel/plugin-transform-template-literals": ^7.12.1 - "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.5.10 - "@storybook/core-client": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/node-logger": 6.5.10 - "@storybook/theming": 6.5.10 - "@storybook/ui": 6.5.10 - "@types/node": ^14.0.10 || ^16.0.0 - "@types/webpack": ^4.41.26 - babel-loader: ^8.0.0 - case-sensitive-paths-webpack-plugin: ^2.3.0 - chalk: ^4.1.0 - core-js: ^3.8.2 - css-loader: ^3.6.0 - express: ^4.17.1 - file-loader: ^6.2.0 - find-up: ^5.0.0 - fs-extra: ^9.0.1 - html-webpack-plugin: ^4.0.0 - node-fetch: ^2.6.7 - pnp-webpack-plugin: 1.6.4 - read-pkg-up: ^7.0.1 - regenerator-runtime: ^0.13.7 - resolve-from: ^5.0.0 - style-loader: ^1.3.0 - telejson: ^6.0.8 - terser-webpack-plugin: ^4.2.3 - ts-dedent: ^2.0.0 - url-loader: ^4.1.1 - util-deprecate: ^1.0.2 - webpack: 4 - webpack-dev-middleware: ^3.7.3 - webpack-virtual-modules: ^0.2.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 954f93dded7a2294cdbd7c7df93fcf1addb34d5daf46d58d8bf64d0a7e83664e34f32cd905b2b2ec771a551869e497c12f8d659a358ac079d815ca02baede6e6 - languageName: node - linkType: hard - -"@storybook/manager-webpack4@npm:6.5.12, @storybook/manager-webpack4@npm:~6.5.12": +"@storybook/manager-webpack4@npm:6.5.12, @storybook/manager-webpack4@npm:~6.5.10, @storybook/manager-webpack4@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/manager-webpack4@npm:6.5.12" dependencies: @@ -8824,7 +7734,7 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:6.5.12": +"@storybook/node-logger@npm:6.5.12, @storybook/node-logger@npm:^6.1.14": version: 6.5.12 resolution: "@storybook/node-logger@npm:6.5.12" dependencies: @@ -8837,28 +7747,6 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:^6.1.14": - version: 6.5.9 - resolution: "@storybook/node-logger@npm:6.5.9" - dependencies: - "@types/npmlog": ^4.1.2 - chalk: ^4.1.0 - core-js: ^3.8.2 - npmlog: ^5.0.1 - pretty-hrtime: ^1.0.3 - checksum: 3f4d236d19f4e99ea75acd405377f7b1a6217964d176c6a3702cfba51ae1ba129d12e66536688457a6c93045f882142a03c87609554f10d8d6c8af4f0ebf9303 - languageName: node - linkType: hard - -"@storybook/postinstall@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/postinstall@npm:6.5.10" - dependencies: - core-js: ^3.8.2 - checksum: ee6355953cb0d4c49392f59502465f967846253afed6df24c845d028e51c0a4b19dadc092a837b29ba8c7fea6529b1ca757b86e579deac7fc3decac2dd8d0247 - languageName: node - linkType: hard - "@storybook/postinstall@npm:6.5.12": version: 6.5.12 resolution: "@storybook/postinstall@npm:6.5.12" @@ -8940,72 +7828,7 @@ __metadata: languageName: node linkType: hard -"@storybook/react@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/react@npm:6.5.10" - dependencies: - "@babel/preset-flow": ^7.12.1 - "@babel/preset-react": ^7.12.10 - "@pmmmwh/react-refresh-webpack-plugin": ^0.5.3 - "@storybook/addons": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/core": 6.5.10 - "@storybook/core-common": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/docs-tools": 6.5.10 - "@storybook/node-logger": 6.5.10 - "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 - "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.10 - "@types/estree": ^0.0.51 - "@types/node": ^14.14.20 || ^16.0.0 - "@types/webpack-env": ^1.16.0 - acorn: ^7.4.1 - acorn-jsx: ^5.3.1 - acorn-walk: ^7.2.0 - babel-plugin-add-react-displayname: ^0.0.5 - babel-plugin-react-docgen: ^4.2.1 - core-js: ^3.8.2 - escodegen: ^2.0.0 - fs-extra: ^9.0.1 - global: ^4.4.0 - html-tags: ^3.1.0 - lodash: ^4.17.21 - prop-types: ^15.7.2 - react-element-to-jsx-string: ^14.3.4 - react-refresh: ^0.11.0 - read-pkg-up: ^7.0.1 - regenerator-runtime: ^0.13.7 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - webpack: ">=4.43.0 <6.0.0" - peerDependencies: - "@babel/core": ^7.11.5 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - require-from-string: ^2.0.2 - peerDependenciesMeta: - "@babel/core": - optional: true - "@storybook/builder-webpack4": - optional: true - "@storybook/builder-webpack5": - optional: true - "@storybook/manager-webpack4": - optional: true - "@storybook/manager-webpack5": - optional: true - typescript: - optional: true - bin: - build-storybook: bin/build.js - start-storybook: bin/index.js - storybook-server: bin/index.js - checksum: 4459ee91ec8aa0159d51e9fae2d0a7bde1be4ff6fd84ae0c1a414feed2d1797e7c8b9f38b256177e3b451142ebbb64a26ac1960a15a5689ae25dbffd3fb1a7b2 - languageName: node - linkType: hard - -"@storybook/react@npm:~6.5.12": +"@storybook/react@npm:~6.5.10, @storybook/react@npm:~6.5.12": version: 6.5.12 resolution: "@storybook/react@npm:6.5.12" dependencies: @@ -9114,28 +7937,7 @@ __metadata: languageName: node linkType: hard -"@storybook/source-loader@npm:6.5.10, @storybook/source-loader@npm:~6.5.10": - version: 6.5.10 - resolution: "@storybook/source-loader@npm:6.5.10" - dependencies: - "@storybook/addons": 6.5.10 - "@storybook/client-logger": 6.5.10 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - core-js: ^3.8.2 - estraverse: ^5.2.0 - global: ^4.4.0 - loader-utils: ^2.0.0 - lodash: ^4.17.21 - prettier: ">=2.2.1 <=2.3.0" - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 77d7a0255cace96fc9953518fe54162ce4b2167b53eb744f498cf2098ba4af8074d75f572940621675303043b69e2281e8a5479ce2d331d47aa86c189cdd53bb - languageName: node - linkType: hard - -"@storybook/source-loader@npm:6.5.12": +"@storybook/source-loader@npm:6.5.12, @storybook/source-loader@npm:~6.5.10": version: 6.5.12 resolution: "@storybook/source-loader@npm:6.5.12" dependencies: @@ -9208,26 +8010,6 @@ __metadata: languageName: node linkType: hard -"@storybook/telemetry@npm:6.5.10": - version: 6.5.10 - resolution: "@storybook/telemetry@npm:6.5.10" - dependencies: - "@storybook/client-logger": 6.5.10 - "@storybook/core-common": 6.5.10 - chalk: ^4.1.0 - core-js: ^3.8.2 - detect-package-manager: ^2.0.1 - fetch-retry: ^5.0.2 - fs-extra: ^9.0.1 - global: ^4.4.0 - isomorphic-unfetch: ^3.1.0 - nanoid: ^3.3.1 - read-pkg-up: ^7.0.1 - regenerator-runtime: ^0.13.7 - checksum: 774acc7f5d91b855be3ec1e2ae5a13b61e3eb9db2c2284ee54d788a701e637a86d4ca14597a027d32555f74392e4c99f47e886bc7729a7222e4e8159c492e054 - languageName: node - linkType: hard - "@storybook/telemetry@npm:6.5.12": version: 6.5.12 resolution: "@storybook/telemetry@npm:6.5.12" @@ -9261,7 +8043,7 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:6.5.10, @storybook/theming@npm:~6.5.10": +"@storybook/theming@npm:6.5.10": version: 6.5.10 resolution: "@storybook/theming@npm:6.5.10" dependencies: @@ -9276,7 +8058,7 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:6.5.12": +"@storybook/theming@npm:6.5.12, @storybook/theming@npm:~6.5.10": version: 6.5.12 resolution: "@storybook/theming@npm:6.5.12" dependencies: @@ -9789,7 +8571,7 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:*": +"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18": version: 4.17.31 resolution: "@types/express-serve-static-core@npm:4.17.31" dependencies: @@ -9800,17 +8582,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.18": - version: 4.17.28 - resolution: "@types/express-serve-static-core@npm:4.17.28" - dependencies: - "@types/node": "*" - "@types/qs": "*" - "@types/range-parser": "*" - checksum: 826489811a5b371c10f02443b4ca894ffc05813bfdf2b60c224f5c18ac9a30a2e518cb9ef9fdfcaa2a1bb17f8bfa4ed1859ccdb252e879c9276271b4ee2df5a9 - languageName: node - linkType: hard - "@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.8": version: 4.17.13 resolution: "@types/express@npm:4.17.13" @@ -10838,16 +9609,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/scope-manager@npm:5.33.0" - dependencies: - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/visitor-keys": 5.33.0 - checksum: b2cbea9abd528d01a5acb2d68a2a5be51ec6827760d3869bdd70920cf6c3a4f9f96d87c77177f8313009d9db71253e4a75f8393f38651e2abaf91ef28e60fb9d - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/scope-manager@npm:5.36.2" @@ -10881,13 +9642,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/types@npm:5.33.0" - checksum: 8bbddda84cb3adf5c659b0d42547a2d6ab87f4eea574aca5dd63a3bd85169f32796ecbddad3b27f18a609070f6b1d18a54018d488bad746ae0f6ea5c02206109 - languageName: node - linkType: hard - "@typescript-eslint/types@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/types@npm:5.36.2" @@ -10913,24 +9667,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.33.0" - dependencies: - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/visitor-keys": 5.33.0 - debug: ^4.3.4 - globby: ^11.1.0 - is-glob: ^4.0.3 - semver: ^7.3.7 - tsutils: ^3.21.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 26f9005cdfb14654125a33d90d872b926820e560dff8970c4629fd5f6f47ad2a31e4c63161564d21bb42a8fc3ced0033994854ee37336ae07d90ccf6300d702b - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/typescript-estree@npm:5.36.2" @@ -10965,7 +9701,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.36.2": +"@typescript-eslint/utils@npm:5.36.2, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": version: 5.36.2 resolution: "@typescript-eslint/utils@npm:5.36.2" dependencies: @@ -10981,22 +9717,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": - version: 5.33.0 - resolution: "@typescript-eslint/utils@npm:5.33.0" - dependencies: - "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.33.0 - "@typescript-eslint/types": 5.33.0 - "@typescript-eslint/typescript-estree": 5.33.0 - eslint-scope: ^5.1.1 - eslint-utils: ^3.0.0 - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 6ce5ee5eabeb6d73538b24e6487f811ecb0ef3467bd366cbd15bf30d904bdedb73fc6f48cf2e2e742dda462b42999ea505e8b59255545825ec9db86f3d423ea7 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:5.30.7": version: 5.30.7 resolution: "@typescript-eslint/visitor-keys@npm:5.30.7" @@ -11007,16 +9727,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.33.0": - version: 5.33.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.33.0" - dependencies: - "@typescript-eslint/types": 5.33.0 - eslint-visitor-keys: ^3.3.0 - checksum: d7e3653de6bac6841e6fcc54226b93ad6bdca4aa76ebe7d83459c016c3eebcc50d4f65ee713174bc267d765295b642d1927a778c5de707b8389e3fcc052aa4a1 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:5.36.2": version: 5.36.2 resolution: "@typescript-eslint/visitor-keys@npm:5.36.2" @@ -12720,19 +11430,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.3.2": - version: 0.3.2 - resolution: "babel-plugin-polyfill-corejs2@npm:0.3.2" - dependencies: - "@babel/compat-data": ^7.17.7 - "@babel/helper-define-polyfill-provider": ^0.3.2 - semver: ^6.1.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a76e7bb1a5cc0a4507baa523c23f9efd75764069a25845beba92290386e5e48ed85b894005ece3b527e13c3d2d9c6589cc0a23befb72ea6fc7aa8711f231bb4d - languageName: node - linkType: hard - "babel-plugin-polyfill-corejs2@npm:^0.3.3": version: 0.3.3 resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3" @@ -12758,18 +11455,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.5.3": - version: 0.5.3 - resolution: "babel-plugin-polyfill-corejs3@npm:0.5.3" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.2 - core-js-compat: ^3.21.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 9c6644a1b0afbe59e402827fdafc6f44994ff92c5b2f258659cbbfd228f7075dea49e95114af10e66d70f36cbde12ff1d81263eb67be749b3ef0e2c18cf3c16d - languageName: node - linkType: hard - "babel-plugin-polyfill-corejs3@npm:^0.6.0": version: 0.6.0 resolution: "babel-plugin-polyfill-corejs3@npm:0.6.0" @@ -12782,17 +11467,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.4.0": - version: 0.4.0 - resolution: "babel-plugin-polyfill-regenerator@npm:0.4.0" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 699aa9c0dc5a2259d7fa52b26613fa1e782439eee54cd98506991f87fddf0c00eec6c5b1917edf586c170731d9e318903bc41210225a691e7bb8087652bbda94 - languageName: node - linkType: hard - "babel-plugin-polyfill-regenerator@npm:^0.4.1": version: 0.4.1 resolution: "babel-plugin-polyfill-regenerator@npm:0.4.1" @@ -13488,22 +12162,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.20.2, browserslist@npm:^4.20.3": - version: 4.20.4 - resolution: "browserslist@npm:4.20.4" - dependencies: - caniuse-lite: ^1.0.30001349 - electron-to-chromium: ^1.4.147 - escalade: ^3.1.1 - node-releases: ^2.0.5 - picocolors: ^1.0.0 - bin: - browserslist: cli.js - checksum: 0e56c42da765524e5c31bc9a1f08afaa8d5dba085071137cf21e56dc78d0cf0283764143df4c7d1c0cd18c3187fc9494e1d93fa0255004f0be493251a28635f9 - languageName: node - linkType: hard - -"browserslist@npm:^4.21.3": +"browserslist@npm:^4.0.0, browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.3": version: 4.21.3 resolution: "browserslist@npm:4.21.3" dependencies: @@ -14009,14 +12668,7 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001349": - version: 1.0.30001352 - resolution: "caniuse-lite@npm:1.0.30001352" - checksum: 575ad031349e56224471859decd100d0f90c804325bf1b543789b212d6126f6e18925766b325b1d96f75e48df0036e68f92af26d1fb175803fd6ad935bc807ac - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001370": +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001370": version: 1.0.30001399 resolution: "caniuse-lite@npm:1.0.30001399" checksum: dd105b06fbbdc89867780a2f4debc3ecb184cff82f35b34aaac486628fcc9cf6bacf37573a9cc22dedc661178d460fa8401374a933cb9d2f8ee67316d98b2a8f @@ -14638,14 +13290,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.0.4": - version: 1.1.1 - resolution: "clsx@npm:1.1.1" - checksum: ff052650329773b9b245177305fc4c4dc3129f7b2be84af4f58dc5defa99538c61d4207be7419405a5f8f3d92007c954f4daba5a7b74e563d5de71c28c830063 - languageName: node - linkType: hard - -"clsx@npm:^1.1.1": +"clsx@npm:^1.0.4, clsx@npm:^1.1.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 @@ -15193,17 +13838,7 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.21.0, core-js-compat@npm:^3.22.1, core-js-compat@npm:^3.8.1": - version: 3.22.8 - resolution: "core-js-compat@npm:3.22.8" - dependencies: - browserslist: ^4.20.3 - semver: 7.0.0 - checksum: 0c82d9110dcb267c2f5547c61b62f8043793d203523048169176b8badf0b73f3792624342b85d9c923df8eb8971b4aa468b160abb81a023d183c5951e4f05a66 - languageName: node - linkType: hard - -"core-js-compat@npm:^3.25.1": +"core-js-compat@npm:^3.25.1, core-js-compat@npm:^3.8.1": version: 3.25.1 resolution: "core-js-compat@npm:3.25.1" dependencies: @@ -17035,13 +15670,6 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.147": - version: 1.4.152 - resolution: "electron-to-chromium@npm:1.4.152" - checksum: d1e3405adc8a8ddbcf5a91f33739f2f3f5fa7612a4e2b6cc6a85d9ebccc87f59cf9e99d2de93c7959b34aa7c633c6c162d912bf895a0e4b79d0c2ce35594948f - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.4.202": version: 1.4.249 resolution: "electron-to-chromium@npm:1.4.249" @@ -17788,7 +16416,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.21.0": +"eslint@npm:^8.21.0, eslint@npm:^8.22.0": version: 8.23.1 resolution: "eslint@npm:8.23.1" dependencies: @@ -17837,7 +16465,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.22.0, eslint@npm:~8.22.0": +"eslint@npm:~8.22.0": version: 8.22.0 resolution: "eslint@npm:8.22.0" dependencies: @@ -17897,18 +16525,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.3.2, espree@npm:^9.3.3": - version: 9.3.3 - resolution: "espree@npm:9.3.3" - dependencies: - acorn: ^8.8.0 - acorn-jsx: ^5.3.2 - eslint-visitor-keys: ^3.3.0 - checksum: 33e8a36fc15d082e68672e322e22a53856b564d60aad8f291a667bfc21b2c900c42412d37dd3c7a0f18b9d0d8f8858dabe8776dbd4b4c2f72c5cf4d6afeabf65 - languageName: node - linkType: hard - -"espree@npm:^9.4.0": +"espree@npm:^9.3.3, espree@npm:^9.4.0": version: 9.4.0 resolution: "espree@npm:9.4.0" dependencies: @@ -18480,14 +17097,7 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.0.0": - version: 3.1.1 - resolution: "fast-redact@npm:3.1.1" - checksum: e486cc9990b5c9724f39bf4e392c1b250c8fd5e8c0145be80c73de3461fc390babe7b48f35746b50bf3cbcd917e093b5685ae66295162c7d9b686a761d48e989 - languageName: node - linkType: hard - -"fast-redact@npm:^3.1.1": +"fast-redact@npm:^3.0.0, fast-redact@npm:^3.1.1": version: 3.1.2 resolution: "fast-redact@npm:3.1.2" checksum: a30eb6b6830333ab213e0def55f46453ca777544dbd3a883016cb590a0eeb95e6fdf546553c1a13d509896bfba889b789991160a6d0996ceb19fce0a02e8b753 @@ -18606,16 +17216,7 @@ __metadata: languageName: node linkType: hard -"fibers@npm:^5.0.1": - version: 5.0.1 - resolution: "fibers@npm:5.0.1" - dependencies: - detect-libc: ^1.0.3 - checksum: 823d148cb993c5aeb7e05e7ed289b9a6073b821bc124c70ac5c6dfee850dc0df65d680a10ce3fb6c37e4705115dcb2d98dad25abde26a2437ada86c5f21be11c - languageName: node - linkType: hard - -"fibers@npm:^5.0.3": +"fibers@npm:^5.0.1, fibers@npm:^5.0.3": version: 5.0.3 resolution: "fibers@npm:5.0.3" dependencies: @@ -26257,14 +24858,7 @@ __metadata: languageName: node linkType: hard -"moment@npm:>= 2.9.0, moment@npm:^2.10.2, moment@npm:^2.29.1": - version: 2.29.3 - resolution: "moment@npm:2.29.3" - checksum: 2e780e36d9a1823c08a1b6313cbb08bd01ecbb2a9062095820a34f42c878991ccba53abaa6abb103fd5c01e763724f295162a8c50b7e95b4f1c992ef0772d3f0 - languageName: node - linkType: hard - -"moment@npm:^2.29.4": +"moment@npm:>= 2.9.0, moment@npm:^2.10.2, moment@npm:^2.29.1, moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e @@ -26889,13 +25483,6 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.5": - version: 2.0.5 - resolution: "node-releases@npm:2.0.5" - checksum: e85d949addd19f8827f32569d2be5751e7812ccf6cc47879d49f79b5234ff4982225e39a3929315f96370823b070640fb04d79fc0ddec8b515a969a03493a42f - languageName: node - linkType: hard - "node-releases@npm:^2.0.6": version: 2.0.6 resolution: "node-releases@npm:2.0.6" @@ -30872,14 +29459,12 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:*, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" +"readable-stream@npm:*, readable-stream@npm:^4.0.0": + version: 4.1.0 + resolution: "readable-stream@npm:4.1.0" dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 + abort-controller: ^3.0.0 + checksum: ff2bb513af6fb43618c8360211b5b9052e25a59e6626d3669c7ba060d021dfffa43c43832e11b18acd6aac15b057c6deae1c41004c1731688c95c455ad02f982 languageName: node linkType: hard @@ -30922,12 +29507,14 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.0.0": - version: 4.1.0 - resolution: "readable-stream@npm:4.1.0" +"readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": + version: 3.6.0 + resolution: "readable-stream@npm:3.6.0" dependencies: - abort-controller: ^3.0.0 - checksum: ff2bb513af6fb43618c8360211b5b9052e25a59e6626d3669c7ba060d021dfffa43c43832e11b18acd6aac15b057c6deae1c41004c1731688c95c455ad02f982 + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 languageName: node linkType: hard @@ -32145,15 +30732,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.0.0": - version: 7.0.0 - resolution: "semver@npm:7.0.0" - bin: - semver: bin/semver.js - checksum: 272c11bf8d083274ef79fe40a81c55c184dff84dd58e3c325299d0927ba48cece1f020793d138382b85f89bab5002a35a5ba59a3a68a7eebbb597eb733838778 - languageName: node - linkType: hard - "semver@npm:7.x, semver@npm:^7.2, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7": version: 7.3.7 resolution: "semver@npm:7.3.7" @@ -34791,7 +33369,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.4.0, tslib@npm:^2.1.0": +"tslib@npm:2.4.0, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1": version: 2.4.0 resolution: "tslib@npm:2.4.0" checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 @@ -34805,7 +33383,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:~2.3.1": +"tslib@npm:~2.3.1": version: 2.3.1 resolution: "tslib@npm:2.3.1" checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 @@ -36074,7 +34652,7 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.10, vm2@npm:^3.9.11": +"vm2@npm:^3.9.11, vm2@npm:^3.9.8": version: 3.9.11 resolution: "vm2@npm:3.9.11" dependencies: @@ -36086,18 +34664,6 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.8": - version: 3.9.9 - resolution: "vm2@npm:3.9.9" - dependencies: - acorn: ^8.7.0 - acorn-walk: ^8.2.0 - bin: - vm2: bin/vm2 - checksum: ea4859565668918b53cf8c087bc18e2a9506f9f4ef919528707a6fecf50a110a2a8c48bc0c7a754c9d12b97cd16775f005b2b941e99d3a52aadfaac5ce77e04d - languageName: node - linkType: hard - "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0" @@ -36907,22 +35473,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3": - version: 8.8.0 - resolution: "ws@npm:8.8.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 6ceed1ca1cb800ef60c7fc8346c7d5d73d73be754228eb958765abf5d714519338efa20ffe674167039486eb3a813aae5a497f8d319e16b4d96216a31df5bd95 - languageName: node - linkType: hard - -"ws@npm:^8.8.1": +"ws@npm:^8.2.3, ws@npm:^8.8.1": version: 8.8.1 resolution: "ws@npm:8.8.1" peerDependencies: -- GitLab From d142e13baf08ce0a7c633b342acccec1365bf7f5 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Fri, 23 Sep 2022 22:45:16 -0300 Subject: [PATCH 075/107] Bump version to 5.2.0-rc.0 --- .github/history.json | 821 +++- HISTORY.md | 5771 ++++++++++++------------- apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 3617 insertions(+), 2983 deletions(-) diff --git a/.github/history.json b/.github/history.json index a7b175f28ab..8a261b327ab 100644 --- a/.github/history.json +++ b/.github/history.json @@ -93667,6 +93667,38 @@ "pull_requests": [] }, "5.1.2": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.8.6": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "5.0.7": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "5.2.0-rc.0": { "node_version": "14.19.3", "npm_version": "6.14.17", "mongo_versions": [ @@ -93676,10 +93708,581 @@ ], "pull_requests": [ { - "pr": "26776", - "title": "[FIX] Livechat trigger messages covering all the website", + "pr": "26940", + "title": "Chore: Bump vm2 to 3.9.11", + "userLogin": "ggazzo", + "milestone": "5.2.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26705", + "title": "[NEW] Matrix Federation events coverage expansion (support for 5 more events)", + "userLogin": "MarcosSpessatto", + "description": "The goal of this PR is to add support for more events on Matrix Federation feature. The new supported events are:\r\n\r\n- Edit Messages;\r\n- Delete Messages\r\n- File Upload (including video and audio/voice messages);\r\n- Send emojis on messages;\r\n- Reactions.", + "milestone": "5.2.0", + "contributors": [ + "MarcosSpessatto", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26867", + "title": "[NEW] Move administration links to an exclusive kebab menu", + "userLogin": "filipemarins", + "description": "Move administration links to an exclusive kebab menu.\r\n\r\n<img width=\"271\" alt=\"Screen Shot 2022-09-14 at 02 59 03\" src=\"https://user-images.githubusercontent.com/9275105/190071665-b4f862d2-bd35-4916-9688-318971c70ab8.png\">", + "milestone": "5.2.0", + "contributors": [ + "filipemarins", + "web-flow" + ] + }, + { + "pr": "26880", + "title": "[FIX] `MongoInvalidArgumentError` on overwriting existing setting", + "userLogin": "debdutdeb", + "milestone": "5.1.3", + "contributors": [ + "debdutdeb", + "sampaiodiego", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26930", + "title": "Regression: Fix open room from current chats", + "userLogin": "ggazzo", + "milestone": "5.2.0", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "26924", + "title": "Chore: Updating apps engine", + "userLogin": "AllanPazRibeiro", + "milestone": "5.2.0", + "contributors": [ + "AllanPazRibeiro", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26931", + "title": "Chore: Convert current-chats to useQuery", + "userLogin": "ggazzo", + "milestone": "5.2.0", + "contributors": [ + "ggazzo", + "tassoevan", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26855", + "title": "Chore: Introduce `useQuery` as data source for the `Room` component", + "userLogin": "tassoevan", + "milestone": "5.2.0", + "contributors": [ + "tassoevan", + "ggazzo" + ] + }, + { + "pr": "26928", + "title": "Regression: wrong permission on livechat/tags endpoints", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "25003", + "title": "Bump actions/cache from 2 to 3.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "26929", + "title": "Chore: Refactor omnichannel livechat tests", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26884", + "title": "Chore: Move micro services to packages", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "26912", + "title": "Chore: fix regressions for omnichannel due room refactor", + "userLogin": "ggazzo", + "milestone": "5.2.0", + "contributors": [ + "ggazzo", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26900", + "title": "[FIX] Message sequential after message thread preview", + "userLogin": "filipemarins", + "milestone": "5.2.0", + "contributors": [ + "filipemarins", + "ggazzo", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26812", + "title": "[FIX] Ephemeral messages not respecting katex setting", + "userLogin": "gabriellsh", + "milestone": "5.2.0", + "contributors": [ + "gabriellsh", + "ggazzo", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "26685", + "title": "[FIX] Old rooms without the associated unit will not be displayed on the current chats", + "userLogin": "murtaza98", + "milestone": "5.2.0", + "contributors": [ + "murtaza98", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26548", + "title": "[IMPROVE] VideoConference Messages UI", + "userLogin": "dougfabris", + "description": "<img width='300px' src='https://user-images.githubusercontent.com/27704687/186758472-f15837be-0a24-470f-8dec-46422da54c6b.png' />\r\n\r\n<img width='300px' src='https://user-images.githubusercontent.com/27704687/186758582-b6d55be9-e555-4a5d-ab21-9322042fbd5a.png' />\r\n\r\n<img width='300px' src='https://user-images.githubusercontent.com/27704687/186758710-19f6bd5f-e0b4-4a6a-aea8-80a1c25f0ce7.png' />", + "milestone": "5.2.0", + "contributors": [ + "dougfabris", + "ggazzo", + "tassoevan", + "pierre-lehnen-rc" + ] + }, + { + "pr": "26829", + "title": "Chore: Omnichannel endpoints to ts", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "26751", + "title": "[NEW] Add Markup to QuoteAttachment", + "userLogin": "hugocostadev", + "milestone": "5.2.0", + "contributors": [ + "hugocostadev", + "filipemarins", + "web-flow", + "gabriellsh" + ] + }, + { + "pr": "26921", + "title": "[FIX] MIME Type fallback for .mov File Extensions", + "userLogin": "hugocostadev", + "description": "Some browsers don't support the MIME type for QuickTime video encoder (.mov), so we had to create a fallback to 'video/mp4'. There are other fallbacks for other browsers, but this is the only one we need for now.\r\n\r\nThe fallback func was used in the MediaPreview and VideoAttachments components", + "milestone": "5.2.0", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "26843", + "title": "[FIX] [Livechat] Unread messages badge", + "userLogin": "tiagoevanp", + "description": "OC-169\r\n\r\nEven if the page was reopening, Livechat will inform unread messages", + "milestone": "5.2.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26917", + "title": "[FIX] Error when mentioning a non-member of a public channel", + "userLogin": "debdutdeb", + "milestone": "5.1.3", + "contributors": [ + "debdutdeb", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26831", + "title": "[IMPROVE] Setting for login email notifications", + "userLogin": "yash-rajpal", + "milestone": "5.2.0", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "26674", + "title": "[NEW] Get moderators, owners and leaders from room scope via apps-engine", + "userLogin": "debdutdeb", + "milestone": "5.2.0", + "contributors": [ + "debdutdeb", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "26877", + "title": "[IMPROVE] Better descriptions for VoIP Settings", + "userLogin": "MartinSchoeler", + "milestone": "5.2.0", + "contributors": [ + "MartinSchoeler", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26822", + "title": "Release 5.1.1", + "userLogin": "tassoevan", + "contributors": [ + "hugocostadev", + "tassoevan", + "MartinSchoeler", + "tiagoevanp" + ] + }, + { + "pr": "26896", + "title": "i18n: Language update from LingoHub 🤖 on 2022-09-19Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "26711", + "title": "[FIX] Upload fails when using WebDav as file storage", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26834", + "title": "[IMPROVE] Updating voip tooltips and icons", + "userLogin": "aleksandernsilva", + "description": "### This PR includes the following tooltip expression changes:\r\nCall toggle button\r\n* Enabled -> Turn off answer calls\r\n* Disabled -> Turn on answer calls\r\n* Signaling connection disconnected -> Waiting for server connection\r\n\r\n\r\nChat toggle button\r\n* Available -> Turn off answer chats\r\n* Not available -> Turn on answer chats\r\n\r\nHold button\r\n* Hold call -> Hold call / Resume call\r\n\r\nMute button\r\n* Mute -> Turn on microphone / Turn off microphone\r\n\r\n### Also includes the following icon changes:\r\n\r\nOld:\r\n\r\n\r\n\r\n\r\n\r\nNew: \r\n\r\n\r\n\r\n", + "milestone": "5.2.0", + "contributors": [ + "aleksandernsilva", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26686", + "title": "Chore: Change BundleChips component appearance", + "userLogin": "rique223", + "description": "# [MKP-44](https://rocketchat.atlassian.net/browse/MKP-44?atlOrigin=eyJpIjoiOTBiNzQ4NzE1ZTJiNDBjMGE0NjQxNmQ2MWNkMTI4YjgiLCJwIjoiaiJ9)\r\nChanged the appearance of the marketplace app bundle chips (E.G.: The blue Enterprise tag in the images bellow).\r\n\r\nDemo image for app details page:\r\n\r\n\r\nDemo image for list view:\r\n", + "contributors": [ + "rique223", + "web-flow" + ] + }, + { + "pr": "26886", + "title": "Chore: merge all functions using autorun x useSubscription pattern", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26860", + "title": "Chore: Fix API tests retry", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "26601", + "title": "[FIX] Check admin setting for whether to display roles or not", + "userLogin": "debdutdeb", + "milestone": "5.2.0", + "contributors": [ + "debdutdeb", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26864", + "title": "Chore: Move Omnichannel Room Footer to react", + "userLogin": "ggazzo", + "contributors": [ + "yash-rajpal", + "MartinSchoeler", + "ggazzo" + ] + }, + { + "pr": "25541", + "title": "Chore: Move presence to package", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26863", + "title": "[IMPROVE] Changed dial pad appearance to match original design", + "userLogin": "aleksandernsilva", + "description": "Before:\r\n\r\n\r\nAfter:\r\n", + "milestone": "5.2.0", + "contributors": [ + "aleksandernsilva", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26757", + "title": "Chore: Move Header to ui-client", + "userLogin": "dougfabris", + "description": "<img width=\"1151\" alt=\"Screen Shot 2022-08-30 at 23 15 18\" src=\"https://user-images.githubusercontent.com/27704687/187577854-08a2c30d-0bd5-48c6-9302-55e28ade96cd.png\">", + "milestone": "5.2.0", + "contributors": [ + "dougfabris", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "26755", + "title": "[FIX] Asset settings description not showing on admin", + "userLogin": "filipemarins", + "milestone": "5.2.0", + "contributors": [ + "filipemarins", + "ggazzo", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26813", + "title": "[FIX] Incorrect filter on contact history search", + "userLogin": "neo-clon", + "contributors": [ + "neo-clon", + "web-flow", + "debdutdeb", + "KevLehman" + ] + }, + { + "pr": "26669", + "title": "[FIX] Unable to send native video recording to Whatsapp", + "userLogin": "murtaza98", + "milestone": "5.2.0", + "contributors": [ + "murtaza98", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "26876", + "title": "Chore: Bump fuselage dependencies and implement new tabs variant in marketplace", + "userLogin": "rique223", + "description": "Bumped the necessary dependencies of fuselage and implemented the new tabs component underline variant.\r\nDemo image:\r\n", + "contributors": [ + "rique223", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26852", + "title": "[FIX] Expanded thread behind sidebar on small screens", + "userLogin": "hugocostadev", + "description": "Sidebar overlapping expanded threads in window sizes between **1135px and 780px** and but the expanded threads should be limited to sidebar size and should not go through it\r\n\r\n### **Actual behavior**\r\n\r\n\r\n\r\n### **Expected behavior**\r\n", + "milestone": "5.2.0", + "contributors": [ + "hugocostadev", + "sampaiodiego", + "web-flow", + "yash-rajpal" + ] + }, + { + "pr": "26709", + "title": "Chore: Upgrading livechat's ui-kit package to latest version", + "userLogin": "aleksandernsilva", + "description": "This PR upgrades Livechat's UiKit package to the version 0.31.16. and adjusts the renderer to be compatible with said version.", + "milestone": "5.2.0", + "contributors": [ + "aleksandernsilva", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26873", + "title": "Chore: fix wrong `test.step` usage", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26841", + "title": "Chore: Update fuselage to next version.", + "userLogin": "gabriellsh", + "milestone": "5.2.0", + "contributors": [ + "gabriellsh", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26836", + "title": "[FIX] Importer fails when file includes user without an email.", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.1.3", + "contributors": [ + "pierre-lehnen-rc", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26845", + "title": "Chore: Rewrite apps WarningModal component to typescript", + "userLogin": "rique223", + "description": "Translated the admin apps WarningModal component from Javascript to Typescript", + "contributors": [ + "rique223", + "web-flow", + "juliajforesti" + ] + }, + { + "pr": "26793", + "title": "Chore: Add RocketChatDesktop function to open video calls when using Electron", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "pierre-lehnen-rc", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26652", + "title": "[FIX] Check if messsage.replies exist on new message template", + "userLogin": "filipemarins", + "milestone": "5.1.3", + "contributors": [ + "filipemarins", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26849", + "title": "i18n: Language update from LingoHub 🤖 on 2022-09-12Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "dougfabris", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26832", + "title": "[IMPROVE] Rounded video attachment", + "userLogin": "yash-rajpal", + "milestone": "5.2.0", + "contributors": [ + "yash-rajpal", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26824", + "title": "[IMPROVE] Include `syncAvatars` on `ldap.syncNow`", + "userLogin": "LucianoPierdona", + "description": "This PR includes a new call for the method `syncAvatars` when `ldap.syncNow` is called", + "milestone": "5.2.0", + "contributors": [ + "LucianoPierdona", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26839", + "title": "Chore: Deprecate some omnichannel meteor methods which aren't getting used", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26846", + "title": "Chore: Configure Prettier for `@rocket.chat/livechat`", "userLogin": "tiagoevanp", - "milestone": "5.1.1", "contributors": [ "tiagoevanp", "kodiakhq[bot]", @@ -93687,12 +94290,121 @@ ] }, { - "pr": "26808", - "title": "[FIX] Restore current chats default table order", - "userLogin": "MartinSchoeler", - "milestone": "5.1.1", + "pr": "26675", + "title": "Chore: `refactor/room`", + "userLogin": "tassoevan", + "description": "Replaces `room` Blaze template with React components.", "contributors": [ - "MartinSchoeler", + "tassoevan" + ] + }, + { + "pr": "26514", + "title": "[NEW] Sections layout and featured apps for marketplace", + "userLogin": "rique223", + "description": "### [MKP??+ - Featured Apps](https://app.clickup.com/t/2t1w9x3)\r\nImplemented a new layout for marketplace's apps list page. Now the page has an All apps section and a \"featured apps\" section in which pre-selected and pre-categorized apps are dynamically shown on the top area of the page to make them stand out.\r\nDemo gif:\r\n\r\n\r\nClickUp Task link: https://app.clickup.com/t/2t1w9x3", + "milestone": "5.2.0", + "contributors": [ + "rique223", + "web-flow" + ] + }, + { + "pr": "26714", + "title": "Chore: first non-aggressive CSS removal", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26749", + "title": "Chore: Cleanup endpoint handlers", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "web-flow", + "murtaza98" + ] + }, + { + "pr": "26826", + "title": "Chore: add aria hidden if modal is open", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26827", + "title": "[FIX] Sign in with Apple on mobile", + "userLogin": "sampaiodiego", + "description": "Our mobile app uses a different method to log in that was removed at #24879 .\r\n\r\nYou can also make it available only on mobile:\r\n\r\n\r\nRemoves the \"Sign in with Apple\" text from the log in button. This was something we thought were required by Apple, but it is actually allowed to have just the logo as all others OAuth we have. Source: https://developer.apple.com/design/human-interface-guidelines/technologies/sign-in-with-apple/buttons/#creating-a-custom-sign-in-with-apple-button", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26673", + "title": "[IMPROVE] Allow delete attachment description on message edit", + "userLogin": "yash-rajpal", + "milestone": "5.2.0", + "contributors": [ + "yash-rajpal", + "hugocostadev", + "web-flow", + "matheusbsilva137", + "kodiakhq[bot]" + ] + }, + { + "pr": "24297", + "title": "[IMPROVE] OTR Message", + "userLogin": "albuquerquefabio", + "milestone": "5.2.0", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "26130", + "title": "[IMPROVE] System messages' consistency", + "userLogin": "matheusbsilva137", + "milestone": "5.2.0", + "contributors": [ + "matheusbsilva137", + "LucianoPierdona", + "web-flow", + "ggazzo", + "kodiakhq[bot]" + ] + }, + { + "pr": "26775", + "title": "[FIX] Menu options margin spacing", + "userLogin": "guijun13", + "description": "- Change margin spacing from 16-4px to 12-8px\r\n\r\n| before | after |\r\n|--------|-------|\r\n|  |  |", + "milestone": "5.2.0", + "contributors": [ + "guijun13", + "web-flow", + "gabriellsh", + "kodiakhq[bot]" + ] + }, + { + "pr": "26805", + "title": "i18n: Language update from LingoHub 🤖 on 2022-09-05Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "dougfabris", "kodiakhq[bot]", "web-flow" ] @@ -93706,8 +94418,99 @@ "contributors": [ "hugocostadev" ] + }, + { + "pr": "26808", + "title": "[FIX] Restore current chats default table order", + "userLogin": "MartinSchoeler", + "milestone": "5.1.1", + "contributors": [ + "MartinSchoeler", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26776", + "title": "[FIX] Livechat trigger messages covering all the website", + "userLogin": "tiagoevanp", + "milestone": "5.1.1", + "contributors": [ + "tiagoevanp", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26748", + "title": "Chore: Refactor message list context usage", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26767", + "title": "Chore: Add tests to cover issue fixed in #26720", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26724", + "title": "Chore: Test/improve reliability", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26768", + "title": "[FIX] Typo on new homepage", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26722", + "title": "i18n: Language update from LingoHub 🤖 on 2022-08-29Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "dougfabris", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26730", + "title": "Chore: Add E2E tests to missing omnichannel endpoints", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26792", + "title": "Merge master into develop & Set version to 5.2.0-develop", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "sampaiodiego", + "web-flow" + ] } ] } } -} \ No newline at end of file +} diff --git a/HISTORY.md b/HISTORY.md index 8d50e59273d..3854ac617f8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,327 @@ +# 5.2.0 (Under Release Candidate Process) + +## 5.2.0-rc.0 +`2022-09-23 · 5 🎉 · 10 🚀 · 21 🛠· 39 🔠· 28 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +### 🎉 New features + + +- Add Markup to QuoteAttachment ([#26751](https://github.com/RocketChat/Rocket.Chat/pull/26751)) + +- Get moderators, owners and leaders from room scope via apps-engine ([#26674](https://github.com/RocketChat/Rocket.Chat/pull/26674)) + +- Matrix Federation events coverage expansion (support for 5 more events) ([#26705](https://github.com/RocketChat/Rocket.Chat/pull/26705)) + + The goal of this PR is to add support for more events on Matrix Federation feature. The new supported events are: + + - Edit Messages; + - Delete Messages + - File Upload (including video and audio/voice messages); + - Send emojis on messages; + - Reactions. + +- Move administration links to an exclusive kebab menu ([#26867](https://github.com/RocketChat/Rocket.Chat/pull/26867)) + + Move administration links to an exclusive kebab menu. + + <img width="271" alt="Screen Shot 2022-09-14 at 02 59 03" src="https://user-images.githubusercontent.com/9275105/190071665-b4f862d2-bd35-4916-9688-318971c70ab8.png"> + +- Sections layout and featured apps for marketplace ([#26514](https://github.com/RocketChat/Rocket.Chat/pull/26514)) + + ### [MKP??+ - Featured Apps](https://app.clickup.com/t/2t1w9x3) + Implemented a new layout for marketplace's apps list page. Now the page has an All apps section and a "featured apps" section in which pre-selected and pre-categorized apps are dynamically shown on the top area of the page to make them stand out. + Demo gif: +  + + ClickUp Task link: https://app.clickup.com/t/2t1w9x3 + +### 🚀 Improvements + + +- Allow delete attachment description on message edit ([#26673](https://github.com/RocketChat/Rocket.Chat/pull/26673)) + +- Better descriptions for VoIP Settings ([#26877](https://github.com/RocketChat/Rocket.Chat/pull/26877)) + +- Changed dial pad appearance to match original design ([#26863](https://github.com/RocketChat/Rocket.Chat/pull/26863)) + + Before: +  + + After: +  + +- Include `syncAvatars` on `ldap.syncNow` ([#26824](https://github.com/RocketChat/Rocket.Chat/pull/26824)) + + This PR includes a new call for the method `syncAvatars` when `ldap.syncNow` is called + +- OTR Message ([#24297](https://github.com/RocketChat/Rocket.Chat/pull/24297)) + +- Rounded video attachment ([#26832](https://github.com/RocketChat/Rocket.Chat/pull/26832)) + +- Setting for login email notifications ([#26831](https://github.com/RocketChat/Rocket.Chat/pull/26831)) + +- System messages' consistency ([#26130](https://github.com/RocketChat/Rocket.Chat/pull/26130)) + +- Updating voip tooltips and icons ([#26834](https://github.com/RocketChat/Rocket.Chat/pull/26834)) + + ### This PR includes the following tooltip expression changes: + Call toggle button + * Enabled -> Turn off answer calls + * Disabled -> Turn on answer calls + * Signaling connection disconnected -> Waiting for server connection + + + Chat toggle button + * Available -> Turn off answer chats + * Not available -> Turn on answer chats + + Hold button + * Hold call -> Hold call / Resume call + + Mute button + * Mute -> Turn on microphone / Turn off microphone + + ### Also includes the following icon changes: + + Old: +  +  +  + + + New: + +  +  +  + +- VideoConference Messages UI ([#26548](https://github.com/RocketChat/Rocket.Chat/pull/26548)) + + <img width='300px' src='https://user-images.githubusercontent.com/27704687/186758472-f15837be-0a24-470f-8dec-46422da54c6b.png' /> + + <img width='300px' src='https://user-images.githubusercontent.com/27704687/186758582-b6d55be9-e555-4a5d-ab21-9322042fbd5a.png' /> + + <img width='300px' src='https://user-images.githubusercontent.com/27704687/186758710-19f6bd5f-e0b4-4a6a-aea8-80a1c25f0ce7.png' /> + +### 🛠Bug fixes + + +- **Livechat:** Unread messages badge ([#26843](https://github.com/RocketChat/Rocket.Chat/pull/26843)) + + OC-169 + + Even if the page was reopening, Livechat will inform unread messages + +- `MongoInvalidArgumentError` on overwriting existing setting ([#26880](https://github.com/RocketChat/Rocket.Chat/pull/26880)) + +- Asset settings description not showing on admin ([#26755](https://github.com/RocketChat/Rocket.Chat/pull/26755)) + +- Check admin setting for whether to display roles or not ([#26601](https://github.com/RocketChat/Rocket.Chat/pull/26601)) + +- Check if messsage.replies exist on new message template ([#26652](https://github.com/RocketChat/Rocket.Chat/pull/26652)) + +- Ephemeral messages not respecting katex setting ([#26812](https://github.com/RocketChat/Rocket.Chat/pull/26812)) + +- Error when mentioning a non-member of a public channel ([#26917](https://github.com/RocketChat/Rocket.Chat/pull/26917)) + +- Expanded thread behind sidebar on small screens ([#26852](https://github.com/RocketChat/Rocket.Chat/pull/26852)) + + Sidebar overlapping expanded threads in window sizes between **1135px and 780px** and but the expanded threads should be limited to sidebar size and should not go through it + + ### **Actual behavior** +  + + + ### **Expected behavior** +  + +- Fix broken legacy message view ([#26819](https://github.com/RocketChat/Rocket.Chat/pull/26819)) + + Fixed `messagesHistory` function, it was filtering messages only with existing threads. + +- Importer fails when file includes user without an email. ([#26836](https://github.com/RocketChat/Rocket.Chat/pull/26836)) + +- Incorrect filter on contact history search ([#26813](https://github.com/RocketChat/Rocket.Chat/pull/26813) by [@neo-clon](https://github.com/neo-clon)) + +- Livechat trigger messages covering all the website ([#26776](https://github.com/RocketChat/Rocket.Chat/pull/26776)) + +- Menu options margin spacing ([#26775](https://github.com/RocketChat/Rocket.Chat/pull/26775)) + + - Change margin spacing from 16-4px to 12-8px + + | before | after | + |--------|-------| + |  |  | + +- Message sequential after message thread preview ([#26900](https://github.com/RocketChat/Rocket.Chat/pull/26900)) + +- MIME Type fallback for .mov File Extensions ([#26921](https://github.com/RocketChat/Rocket.Chat/pull/26921)) + + Some browsers don't support the MIME type for QuickTime video encoder (.mov), so we had to create a fallback to 'video/mp4'. There are other fallbacks for other browsers, but this is the only one we need for now. + + The fallback func was used in the MediaPreview and VideoAttachments components + +- Old rooms without the associated unit will not be displayed on the current chats ([#26685](https://github.com/RocketChat/Rocket.Chat/pull/26685)) + +- Restore current chats default table order ([#26808](https://github.com/RocketChat/Rocket.Chat/pull/26808)) + +- Sign in with Apple on mobile ([#26827](https://github.com/RocketChat/Rocket.Chat/pull/26827)) + + Our mobile app uses a different method to log in that was removed at #24879 . + + You can also make it available only on mobile: +  + + Removes the "Sign in with Apple" text from the log in button. This was something we thought were required by Apple, but it is actually allowed to have just the logo as all others OAuth we have. Source: https://developer.apple.com/design/human-interface-guidelines/technologies/sign-in-with-apple/buttons/#creating-a-custom-sign-in-with-apple-button + +- Typo on new homepage ([#26768](https://github.com/RocketChat/Rocket.Chat/pull/26768)) + +- Unable to send native video recording to Whatsapp ([#26669](https://github.com/RocketChat/Rocket.Chat/pull/26669)) + +- Upload fails when using WebDav as file storage ([#26711](https://github.com/RocketChat/Rocket.Chat/pull/26711)) + +<details> +<summary>🔠Minor changes</summary> + + +- Bump actions/cache from 2 to 3.0.1 ([#25003](https://github.com/RocketChat/Rocket.Chat/pull/25003) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore: `refactor/room` ([#26675](https://github.com/RocketChat/Rocket.Chat/pull/26675)) + + Replaces `room` Blaze template with React components. + +- Chore: add aria hidden if modal is open ([#26826](https://github.com/RocketChat/Rocket.Chat/pull/26826)) + +- Chore: Add E2E tests to missing omnichannel endpoints ([#26730](https://github.com/RocketChat/Rocket.Chat/pull/26730)) + +- Chore: Add RocketChatDesktop function to open video calls when using Electron ([#26793](https://github.com/RocketChat/Rocket.Chat/pull/26793)) + +- Chore: Add tests to cover issue fixed in #26720 ([#26767](https://github.com/RocketChat/Rocket.Chat/pull/26767)) + +- Chore: Bump fuselage dependencies and implement new tabs variant in marketplace ([#26876](https://github.com/RocketChat/Rocket.Chat/pull/26876)) + + Bumped the necessary dependencies of fuselage and implemented the new tabs component underline variant. + Demo image: +  + +- Chore: Bump vm2 to 3.9.11 ([#26940](https://github.com/RocketChat/Rocket.Chat/pull/26940)) + +- Chore: Change BundleChips component appearance ([#26686](https://github.com/RocketChat/Rocket.Chat/pull/26686)) + + # [MKP-44](https://rocketchat.atlassian.net/browse/MKP-44?atlOrigin=eyJpIjoiOTBiNzQ4NzE1ZTJiNDBjMGE0NjQxNmQ2MWNkMTI4YjgiLCJwIjoiaiJ9) + Changed the appearance of the marketplace app bundle chips (E.G.: The blue Enterprise tag in the images bellow). + + Demo image for app details page: +  + + Demo image for list view: +  + +- Chore: Cleanup endpoint handlers ([#26749](https://github.com/RocketChat/Rocket.Chat/pull/26749)) + +- Chore: Configure Prettier for `@rocket.chat/livechat` ([#26846](https://github.com/RocketChat/Rocket.Chat/pull/26846)) + +- Chore: Convert current-chats to useQuery ([#26931](https://github.com/RocketChat/Rocket.Chat/pull/26931)) + +- Chore: Deprecate some omnichannel meteor methods which aren't getting used ([#26839](https://github.com/RocketChat/Rocket.Chat/pull/26839)) + +- Chore: first non-aggressive CSS removal ([#26714](https://github.com/RocketChat/Rocket.Chat/pull/26714)) + +- Chore: Fix API tests retry ([#26860](https://github.com/RocketChat/Rocket.Chat/pull/26860)) + +- Chore: fix regressions for omnichannel due room refactor ([#26912](https://github.com/RocketChat/Rocket.Chat/pull/26912)) + +- Chore: fix wrong `test.step` usage ([#26873](https://github.com/RocketChat/Rocket.Chat/pull/26873)) + +- Chore: Introduce `useQuery` as data source for the `Room` component ([#26855](https://github.com/RocketChat/Rocket.Chat/pull/26855)) + +- Chore: merge all functions using autorun x useSubscription pattern ([#26886](https://github.com/RocketChat/Rocket.Chat/pull/26886)) + +- Chore: Move Header to ui-client ([#26757](https://github.com/RocketChat/Rocket.Chat/pull/26757)) + + <img width="1151" alt="Screen Shot 2022-08-30 at 23 15 18" src="https://user-images.githubusercontent.com/27704687/187577854-08a2c30d-0bd5-48c6-9302-55e28ade96cd.png"> + +- Chore: Move micro services to packages ([#26884](https://github.com/RocketChat/Rocket.Chat/pull/26884)) + +- Chore: Move Omnichannel Room Footer to react ([#26864](https://github.com/RocketChat/Rocket.Chat/pull/26864)) + +- Chore: Move presence to package ([#25541](https://github.com/RocketChat/Rocket.Chat/pull/25541)) + +- Chore: Omnichannel endpoints to ts ([#26829](https://github.com/RocketChat/Rocket.Chat/pull/26829)) + +- Chore: Refactor message list context usage ([#26748](https://github.com/RocketChat/Rocket.Chat/pull/26748)) + +- Chore: Refactor omnichannel livechat tests ([#26929](https://github.com/RocketChat/Rocket.Chat/pull/26929)) + +- Chore: Rewrite apps WarningModal component to typescript ([#26845](https://github.com/RocketChat/Rocket.Chat/pull/26845)) + + Translated the admin apps WarningModal component from Javascript to Typescript + +- Chore: Test/improve reliability ([#26724](https://github.com/RocketChat/Rocket.Chat/pull/26724)) + +- Chore: Update fuselage to next version. ([#26841](https://github.com/RocketChat/Rocket.Chat/pull/26841)) + +- Chore: Updating apps engine ([#26924](https://github.com/RocketChat/Rocket.Chat/pull/26924)) + +- Chore: Upgrading livechat's ui-kit package to latest version ([#26709](https://github.com/RocketChat/Rocket.Chat/pull/26709)) + + This PR upgrades Livechat's UiKit package to the version 0.31.16. and adjusts the renderer to be compatible with said version. + +- i18n: Language update from LingoHub 🤖 on 2022-08-29Z ([#26722](https://github.com/RocketChat/Rocket.Chat/pull/26722)) + +- i18n: Language update from LingoHub 🤖 on 2022-09-05Z ([#26805](https://github.com/RocketChat/Rocket.Chat/pull/26805)) + +- i18n: Language update from LingoHub 🤖 on 2022-09-12Z ([#26849](https://github.com/RocketChat/Rocket.Chat/pull/26849)) + +- i18n: Language update from LingoHub 🤖 on 2022-09-19Z ([#26896](https://github.com/RocketChat/Rocket.Chat/pull/26896)) + +- Merge master into develop & Set version to 5.2.0-develop ([#26792](https://github.com/RocketChat/Rocket.Chat/pull/26792)) + +- Regression: Fix open room from current chats ([#26930](https://github.com/RocketChat/Rocket.Chat/pull/26930)) + +- Regression: wrong permission on livechat/tags endpoints ([#26928](https://github.com/RocketChat/Rocket.Chat/pull/26928)) + +- Release 5.1.1 ([#26822](https://github.com/RocketChat/Rocket.Chat/pull/26822)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Contributors 😠+ +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@neo-clon](https://github.com/neo-clon) + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) +- [@KevLehman](https://github.com/KevLehman) +- [@LucianoPierdona](https://github.com/LucianoPierdona) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@albuquerquefabio](https://github.com/albuquerquefabio) +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@d-gubert](https://github.com/d-gubert) +- [@debdutdeb](https://github.com/debdutdeb) +- [@dougfabris](https://github.com/dougfabris) +- [@filipemarins](https://github.com/filipemarins) +- [@gabriellsh](https://github.com/gabriellsh) +- [@ggazzo](https://github.com/ggazzo) +- [@guijun13](https://github.com/guijun13) +- [@hugocostadev](https://github.com/hugocostadev) +- [@jeanfbrito](https://github.com/jeanfbrito) +- [@juliajforesti](https://github.com/juliajforesti) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@rique223](https://github.com/rique223) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@souzaramon](https://github.com/souzaramon) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) + # 5.1.2 -`2022-09-12 · 4 🛠· 4 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` +`2022-09-12 · 1 🛠· 1 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` ### Engine versions - Node: `14.19.3` @@ -9,14 +330,15 @@ ### 🛠Bug fixes + - Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) -### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Contributors 😠+### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 - [@ggazzo](https://github.com/ggazzo) # 5.1.1 -`2022-09-06 · 3 🛠· 3 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` +`2022-09-08 · 3 🛠· 3 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` ### Engine versions - Node: `14.19.3` @@ -63,30 +385,30 @@ - Capability to search visitors by custom fields ([#26312](https://github.com/RocketChat/Rocket.Chat/pull/26312)) - Users of the endpoints [api/v1/omnichannel/contact.search](https://developer.rocket.chat/reference/api/rest-api/endpoints/omnichannel/livechat-endpoints/livechat-contact/omnichannel-search-contact) and [/api/v1/livechat/visitors.search](https://developer.rocket.chat/reference/api/rest-api/endpoints/omnichannel/livechat-endpoints/visitor/search-for-visitors) are now able to search by custom fields in their objects. + Users of the endpoints [api/v1/omnichannel/contact.search](https://developer.rocket.chat/reference/api/rest-api/endpoints/omnichannel/livechat-endpoints/livechat-contact/omnichannel-search-contact) and [/api/v1/livechat/visitors.search](https://developer.rocket.chat/reference/api/rest-api/endpoints/omnichannel/livechat-endpoints/visitor/search-for-visitors) are now able to search by custom fields in their objects. Capability of selecting if a custom field can be searched for is added in the Omnichannel pannel as a toggle for `searchable`, the included JSON in the Accounts' Custom Field example has been updated to make it explicit for future configurations that the field has to be enabled as searchable for that to happen. - Fallback Error component for Engagement Dashboard widgets ([#26441](https://github.com/RocketChat/Rocket.Chat/pull/26441)) - As proposed, was added a fallback component to catch errors at Engagement Dashboard widgets individually. - It used an Error boundary to catch `react-query` errors, due to this scenario was necessary to install and use the library [react-error-boundary](https://github.com/bvaughn/react-error-boundary) that implements everything and more compared to our ErrorBoundary component, the main reason was to capture Query errors and the implementation with `react-query` library. - - **New layout:** - - Before: -  - - After: + As proposed, was added a fallback component to catch errors at Engagement Dashboard widgets individually. + It used an Error boundary to catch `react-query` errors, due to this scenario was necessary to install and use the library [react-error-boundary](https://github.com/bvaughn/react-error-boundary) that implements everything and more compared to our ErrorBoundary component, the main reason was to capture Query errors and the implementation with `react-query` library. + + **New layout:** + + Before: +  + + After:  - Marketplace apps page new list view layout ([#26181](https://github.com/RocketChat/Rocket.Chat/pull/26181)) - Refactored the layout of the marketplace list of apps, now it has a more minimalist and flexbox-based style. Also implemented a new status filter. - - Demo gif: -  - - ClickUp task: + Refactored the layout of the marketplace list of apps, now it has a more minimalist and flexbox-based style. Also implemented a new status filter. + + Demo gif: +  + + ClickUp task: https://app.clickup.com/t/1na7437 - Surface featured apps endpoint ([#26416](https://github.com/RocketChat/Rocket.Chat/pull/26416)) @@ -100,47 +422,33 @@ - Added identification on calls to/from existing contacts ([#26334](https://github.com/RocketChat/Rocket.Chat/pull/26334)) - Before: - <img width="273" alt="Screen Shot 2022-07-02 at 01 50 52" src="https://user-images.githubusercontent.com/6494543/180296563-393ae4e8-2d91-4e21-8567-3112d445b8ee.png"> - - After: + Before: + <img width="273" alt="Screen Shot 2022-07-02 at 01 50 52" src="https://user-images.githubusercontent.com/6494543/180296563-393ae4e8-2d91-4e21-8567-3112d445b8ee.png"> + + After: <img width="279" alt="Screen Shot 2022-07-21 at 16 00 27" src="https://user-images.githubusercontent.com/6494543/180296628-188ba5b2-6f49-450f-b0b0-fd8b40d1b45e.png"> - General federation improvements ([#26150](https://github.com/RocketChat/Rocket.Chat/pull/26150)) - I know this changed a lot of files, but the main goal for this PR is not to change any behavior, the goals for the PR are: - - - - Refactor the code; - - - Solve any tech debt; - - - Simplify and reuse some parts of the code; - - - Remove duplicated code; - - - Remove all unsafe type castings; - - - Solve all Eslint errors and warnings; - - - Split too big files; - - - Encapsulate the business logic in a better way, avoiding exposing and leaking internal logic to the unintended layers; - - - Improve the actual test cases; - - - Add more test cases, since a lot of cases were omitted during the release phase; - + I know this changed a lot of files, but the main goal for this PR is not to change any behavior, the goals for the PR are: + + - Refactor the code; + - Solve any tech debt; + - Simplify and reuse some parts of the code; + - Remove duplicated code; + - Remove all unsafe type castings; + - Solve all Eslint errors and warnings; + - Split too big files; + - Encapsulate the business logic in a better way, avoiding exposing and leaking internal logic to the unintended layers; + - Improve the actual test cases; + - Add more test cases, since a lot of cases were omitted during the release phase; - Remove unsafe `Object.assign` statements and prefer to use the class `constructor` instead; - New 'not found page' design ([#26452](https://github.com/RocketChat/Rocket.Chat/pull/26452)) - - Add a new design for the not-found page - - - Add English translation for "page not found" and "Homepage" - - - Update English translation for "Room_not_exist_or_not_permission" - + - Add a new design for the not-found page + - Add English translation for "page not found" and "Homepage" + - Update English translation for "Room_not_exist_or_not_permission" - Add "Homepage" button on the room not found page - OTR refactoring ([#24757](https://github.com/RocketChat/Rocket.Chat/pull/24757)) @@ -174,7 +482,7 @@ - Allow normal user to open apps contextual bar ([#26495](https://github.com/RocketChat/Rocket.Chat/pull/26495)) - Fix the bug where normal users cannot open an app contextual bar. + Fix the bug where normal users cannot open an app contextual bar. The request made by the contextual bar to get the app information, which was for admin only, was removed since the response was not being used. - Autotranslate method should respect setting ([#26549](https://github.com/RocketChat/Rocket.Chat/pull/26549)) @@ -195,9 +503,9 @@ - Decrypt E2EE messages on thread list ([#26133](https://github.com/RocketChat/Rocket.Chat/pull/26133)) - ### Before - <img width="672" alt="Screenshot 2022-07-05 at 9 28 22 PM" src="https://user-images.githubusercontent.com/58601732/177369226-b863a362-4064-450e-8c93-ff708b378c7b.png"> - ### After + ### Before + <img width="672" alt="Screenshot 2022-07-05 at 9 28 22 PM" src="https://user-images.githubusercontent.com/58601732/177369226-b863a362-4064-450e-8c93-ff708b378c7b.png"> + ### After <img width="674" alt="Screenshot 2022-07-05 at 9 27 42 PM" src="https://user-images.githubusercontent.com/58601732/177369298-fc6b375a-687b-4b84-8600-c0a179637f2a.png"> - Default BH not getting applied in-case any other BH is disabled ([#26471](https://github.com/RocketChat/Rocket.Chat/pull/26471)) @@ -214,12 +522,12 @@ - MDM content alignment ([#26665](https://github.com/RocketChat/Rocket.Chat/pull/26665)) - - remove left margin of MDM content - - before: -  - - after: + - remove left margin of MDM content + + before: +  + + after: <img width="610" alt="Screen Shot 2022-08-23 at 11 50 55" src="https://user-images.githubusercontent.com/48109548/186213460-c057e8a1-5838-44f5-b3fa-9f76752f0a89.png"> - Missing bio field UI validation ([#26345](https://github.com/RocketChat/Rocket.Chat/pull/26345)) @@ -270,16 +578,14 @@ - UI fixes on dropdown titles ([#26318](https://github.com/RocketChat/Rocket.Chat/pull/26318)) - - Add paddings on profile dropdown title - - - Fix paddings on 'sort' and 'create new' dropdown titles - - - Remove inline styles of `OptionTitle` (removing uppercase style) - - | Location | Before | After | - | --------------- | --------------- | --------------- | - | Sort Dropdown |  | <img width="178" alt="Screen Shot 2022-08-05 at 15 54 14" src="https://user-images.githubusercontent.com/48109548/183143281-52205a07-e264-4a92-85b9-cb750623aabe.png"> | - | User Dropdown |  | <img width="244" alt="Screen Shot 2022-08-05 at 15 54 05" src="https://user-images.githubusercontent.com/48109548/183143288-65ab1bc3-6cd7-4318-b973-9b4307e3dbf6.png"> | + - Add paddings on profile dropdown title + - Fix paddings on 'sort' and 'create new' dropdown titles + - Remove inline styles of `OptionTitle` (removing uppercase style) + + | Location | Before | After | + | --------------- | --------------- | --------------- | + | Sort Dropdown |  | <img width="178" alt="Screen Shot 2022-08-05 at 15 54 14" src="https://user-images.githubusercontent.com/48109548/183143281-52205a07-e264-4a92-85b9-cb750623aabe.png"> | + | User Dropdown |  | <img width="244" alt="Screen Shot 2022-08-05 at 15 54 05" src="https://user-images.githubusercontent.com/48109548/183143288-65ab1bc3-6cd7-4318-b973-9b4307e3dbf6.png"> | | Create new Dropdown | <img width="173" alt="Screen Shot 2022-08-08 at 11 33 17" src="https://user-images.githubusercontent.com/48109548/183442902-e8586b2c-795b-4dfe-9045-c2c7a8a8194a.png"> | <img width="164" alt="Screen Shot 2022-08-05 at 15 54 26" src="https://user-images.githubusercontent.com/48109548/183143273-ece23507-9b60-4c09-a4fe-dcea00454cf4.png"> | - Unable to remove a user who joined a public team with a mention ([#26218](https://github.com/RocketChat/Rocket.Chat/pull/26218)) @@ -354,11 +660,9 @@ - Chore: Engagement Dashboard end to end tests ([#26702](https://github.com/RocketChat/Rocket.Chat/pull/26702)) - Adding tests to check the behavior of the Engagement Dashboard for the Enterprise Edition license. - The tests include: - - - Visibility and navigation of page and tabs - + Adding tests to check the behavior of the Engagement Dashboard for the Enterprise Edition license. + The tests include: + - Visibility and navigation of page and tabs - Fallback component on widgets error - Chore: ESLint warnings ([#26504](https://github.com/RocketChat/Rocket.Chat/pull/26504)) @@ -453,10 +757,10 @@ - Chore: Remove italic/bold font-style from system messages ([#26655](https://github.com/RocketChat/Rocket.Chat/pull/26655)) - It was removed from system messages font-styles elements (italic and bold) that highlighted some words as `users`, `room_name` and others. - - In addition to this PR, was also created a PR to Fuselage to remove italic font style in general at system messages. - + It was removed from system messages font-styles elements (italic and bold) that highlighted some words as `users`, `room_name` and others. + + In addition to this PR, was also created a PR to Fuselage to remove italic font style in general at system messages. + Fuselage PR: https://github.com/RocketChat/fuselage/pull/830 - Chore: Remove Livechat Dashboard Templates ([#26627](https://github.com/RocketChat/Rocket.Chat/pull/26627)) @@ -487,17 +791,17 @@ - Chore: Rewrite SaveToWebdav Modal to React Component ([#24365](https://github.com/RocketChat/Rocket.Chat/pull/24365)) - ### before -  - - ### after + ### before +  + + ### after  - Chore: Rewrite VerticalBarOldActions to TS ([#26277](https://github.com/RocketChat/Rocket.Chat/pull/26277)) - Chore: Separating user edit form to prevent browser autocomplete ([#26280](https://github.com/RocketChat/Rocket.Chat/pull/26280)) - Separating user edit form to prevent browser password and username auto-complete. + Separating user edit form to prevent browser password and username auto-complete. The browser will continue showing the suggestion dropdown for the password field, but when you select a suggestion the other text field will not be impacted, as was happening before with 'Nickname' field - Chore: skipping tests that are based on kebab menu ([#26616](https://github.com/RocketChat/Rocket.Chat/pull/26616)) @@ -532,8 +836,8 @@ - i18n: Fix Korean set role translation ([#24966](https://github.com/RocketChat/Rocket.Chat/pull/24966) by [@imyaman](https://github.com/imyaman)) - English https://pbs.twimg.com/media/FO2zby1aQAMB84D?format=png&name=small - Korean https://pbs.twimg.com/media/FO2zWgKaIAYidJ7?format=png&name=small + English https://pbs.twimg.com/media/FO2zby1aQAMB84D?format=png&name=small + Korean https://pbs.twimg.com/media/FO2zWgKaIAYidJ7?format=png&name=small Google Translate https://pbs.twimg.com/media/FO20MPnaUAU-TU_?format=jpg&name=medium - i18n: Language update from LingoHub 🤖 on 2022-08-01Z ([#26429](https://github.com/RocketChat/Rocket.Chat/pull/26429)) @@ -570,11 +874,11 @@ - Regression: Fix spacing problem on AppStatus component ([#26421](https://github.com/RocketChat/Rocket.Chat/pull/26421)) - Fixed a problem where the AppStatus component would show a unwanted margin when an app was installed and had an update. - Before: -  - - After: + Fixed a problem where the AppStatus component would show a unwanted margin when an app was installed and had an update. + Before: +  + + After:  - Regression: Home cards UI tweaks ([#26610](https://github.com/RocketChat/Rocket.Chat/pull/26610)) @@ -739,19 +1043,13 @@ - Endpoints not working when using "Use Real Name" setting ([#26530](https://github.com/RocketChat/Rocket.Chat/pull/26530)) - The list of endpoints affected is: - - - - `/api/v1/channels.list` - - - `/api/v1/channels.list.joined` - - - `/api/v1/groups.list` - - - `/api/v1/groups.listAll` - - - `/api/v1/im.list` - + The list of endpoints affected is: + + - `/api/v1/channels.list` + - `/api/v1/channels.list.joined` + - `/api/v1/groups.list` + - `/api/v1/groups.listAll` + - `/api/v1/im.list` - `/api/v1/im.list.everyone` - LDAP fails to sync teams when the user DN has escaped characters. ([#26535](https://github.com/RocketChat/Rocket.Chat/pull/26535)) @@ -834,7 +1132,7 @@ - Regression: Fix app privacy links opening in desktop client instead of browser ([#26368](https://github.com/RocketChat/Rocket.Chat/pull/26368)) - Demo gif: + Demo gif:  - Release 5.0.1 ([#26450](https://github.com/RocketChat/Rocket.Chat/pull/26450)) @@ -872,38 +1170,32 @@ - Remove show message in main thread preference ([#26002](https://github.com/RocketChat/Rocket.Chat/pull/26002)) - This PR removes the confusion between the `show message in main thread` and the function `also to send to channel`. In the past, we used the `show message in main thread` as a solution to help users to understand the thread feature, as this feature is now mature enough there's no reason to maintain this preference. - - Send the thread message to the main channel or just inside of the thread, should be a decision from the user where the function `also send to channel` appears. Because of that, and because of a bunch of requests and issues we received, we're introducing a new preference `also send thread to channel` where users will be able to decide the behavior of the checkbox. - -  - - Now there are three behavior options - - - `Default`: when it unchecks after sending the first message - <img width='250px' height='350px' src='https://user-images.githubusercontent.com/27704687/175656500-34817639-7f13-4641-b4fa-9dd106e99443.gif' /> - - - - `Always`: stay checked for all messages - <img width='250px' height='350px' src='https://user-images.githubusercontent.com/27704687/175657299-d88efaba-1c2b-4bb9-a23a-f9755dcec5ca.gif' /> - - - - `Never`: stay unchecked for all messages + This PR removes the confusion between the `show message in main thread` and the function `also to send to channel`. In the past, we used the `show message in main thread` as a solution to help users to understand the thread feature, as this feature is now mature enough there's no reason to maintain this preference. + + Send the thread message to the main channel or just inside of the thread, should be a decision from the user where the function `also send to channel` appears. Because of that, and because of a bunch of requests and issues we received, we're introducing a new preference `also send thread to channel` where users will be able to decide the behavior of the checkbox. + +  + + Now there are three behavior options + - `Default`: when it unchecks after sending the first message + <img width='250px' height='350px' src='https://user-images.githubusercontent.com/27704687/175656500-34817639-7f13-4641-b4fa-9dd106e99443.gif' /> + + - `Always`: stay checked for all messages + <img width='250px' height='350px' src='https://user-images.githubusercontent.com/27704687/175657299-d88efaba-1c2b-4bb9-a23a-f9755dcec5ca.gif' /> + + - `Never`: stay unchecked for all messages <img width='250px' height='350px' src='https://user-images.githubusercontent.com/27704687/175657544-3dcd0adc-05cf-4196-83a6-f6cc29a1de2b.gif' /> - Remove support to old MongoDB versions ([#26098](https://github.com/RocketChat/Rocket.Chat/pull/26098)) - As per MongoDB Lifecycle Schedules (https://www.mongodb.com/support-policy/lifecycles) we're removing official support to MongoDB versions **3.6 and 4.0** that have already reached end-of-life. - - As MongoDB 4.2 was a "supported" version before Rocket.Chat 5.0, we'll continue supporting it, but will be flagged as deprecated. We recommend upgrading to MongoDB 4.4+. - - Here are official docs on how to upgrade to some of the supported versions: - - - - https://www.mongodb.com/docs/manual/release-notes/4.2-upgrade-replica-set/ - - - https://www.mongodb.com/docs/v4.4/release-notes/4.4-upgrade-replica-set/ - + As per MongoDB Lifecycle Schedules (https://www.mongodb.com/support-policy/lifecycles) we're removing official support to MongoDB versions **3.6 and 4.0** that have already reached end-of-life. + + As MongoDB 4.2 was a "supported" version before Rocket.Chat 5.0, we'll continue supporting it, but will be flagged as deprecated. We recommend upgrading to MongoDB 4.4+. + + Here are official docs on how to upgrade to some of the supported versions: + + - https://www.mongodb.com/docs/manual/release-notes/4.2-upgrade-replica-set/ + - https://www.mongodb.com/docs/v4.4/release-notes/4.4-upgrade-replica-set/ - https://www.mongodb.com/docs/manual/release-notes/5.0-upgrade-replica-set/ - remove unused endpoints and restify others ([#25889](https://github.com/RocketChat/Rocket.Chat/pull/25889)) @@ -924,28 +1216,26 @@ - VideoConference ([#25570](https://github.com/RocketChat/Rocket.Chat/pull/25570)) - In this PR we're deprecating the Video Conference functionality from the core of the application and introducing a **new video conference flow**: - - <img src='https://user-images.githubusercontent.com/27704687/176227619-fd7603e5-dc0b-4089-b811-749313b6e674.gif' /> - - Now the video conference feature will be agnostic so you'll be able to set the provider such as **Jisti** and **BBB** as apps from our marketplace: - - <img width='70%' src='https://user-images.githubusercontent.com/27704687/176220152-a88dac4e-75ba-4fd5-9d4e-266316d4cb07.png' /> - - Video conferences settings are now global, allowing you to set the default provider - - <img width='70%' src='https://user-images.githubusercontent.com/27704687/176220808-a8213628-2168-4c4e-9679-d858215dd4cb.png' /> - - ### [Enterprise Features] - - - Video Conferences List - <img width='300px' height='450' src='https://user-images.githubusercontent.com/27704687/176223821-7a2a280a-149f-4645-ac85-bbdf5b34f311.png' /> - - - - Ringing function for direct messages - - <img width='70%' src='https://user-images.githubusercontent.com/27704687/176225059-de48a881-5ff4-45ad-abf2-8a7827dd0b1c.gif' /> - + In this PR we're deprecating the Video Conference functionality from the core of the application and introducing a **new video conference flow**: + + <img src='https://user-images.githubusercontent.com/27704687/176227619-fd7603e5-dc0b-4089-b811-749313b6e674.gif' /> + + Now the video conference feature will be agnostic so you'll be able to set the provider such as **Jisti** and **BBB** as apps from our marketplace: + + <img width='70%' src='https://user-images.githubusercontent.com/27704687/176220152-a88dac4e-75ba-4fd5-9d4e-266316d4cb07.png' /> + + Video conferences settings are now global, allowing you to set the default provider + + <img width='70%' src='https://user-images.githubusercontent.com/27704687/176220808-a8213628-2168-4c4e-9679-d858215dd4cb.png' /> + + ### [Enterprise Features] + - Video Conferences List + <img width='300px' height='450' src='https://user-images.githubusercontent.com/27704687/176223821-7a2a280a-149f-4645-ac85-bbdf5b34f311.png' /> + + - Ringing function for direct messages + + <img width='70%' src='https://user-images.githubusercontent.com/27704687/176225059-de48a881-5ff4-45ad-abf2-8a7827dd0b1c.gif' /> + <img width='70%' src='https://user-images.githubusercontent.com/27704687/176225530-3a0f6149-5ee9-425b-b841-27d35aed8165.gif' /> ### 🎉 New features @@ -955,9 +1245,9 @@ - **APPS:** Allow dispatchment of actions from input elements ([#25949](https://github.com/RocketChat/Rocket.Chat/pull/25949)) - This allows for apps receiving block actions when a user types on a plain text input field or selects an item from the static. A debounce of 700 ms is done when listening for typing action so the app is not flooded with actions. - - + This allows for apps receiving block actions when a user types on a plain text input field or selects an item from the static. A debounce of 700 ms is done when listening for typing action so the app is not flooded with actions. + + https://user-images.githubusercontent.com/733282/174858175-5ea53046-c791-493e-859b-b80431e94ffa.mp4 - **APPS:** Allowing apps to register authenticated routes ([#25937](https://github.com/RocketChat/Rocket.Chat/pull/25937)) @@ -968,10 +1258,10 @@ - **ENTERPRISE:** Introducing dial pad component into sidebar, calls table, contextual bar ([#26081](https://github.com/RocketChat/Rocket.Chat/pull/26081)) - This PR adds a new call button that can be used from Sidebar & Contact Center. This also enables Omnichannel agents to make outbound calls from within Rocket.Chat. - - Depending on your server and call server configuration, you can do international calling, national and domestic calling. - + This PR adds a new call button that can be used from Sidebar & Contact Center. This also enables Omnichannel agents to make outbound calls from within Rocket.Chat. + + Depending on your server and call server configuration, you can do international calling, national and domestic calling. + The buttons on Contact Center allows an agent to call an existing number without having to type the number again. - Ability for RC server to check the business hour for a specific department ([#25436](https://github.com/RocketChat/Rocket.Chat/pull/25436)) @@ -992,8 +1282,8 @@ - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) - Experimental support for Matrix Federation with a Bridge - + Experimental support for Matrix Federation with a Bridge + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 - Colors Palette - Buttons ([#25626](https://github.com/RocketChat/Rocket.Chat/pull/25626)) @@ -1002,8 +1292,8 @@ - Create releases tab in the marketplace app info page ([#25965](https://github.com/RocketChat/Rocket.Chat/pull/25965)) - Added a Releases tab to the app info page of installed marketplace apps. This tab will show all the released versions of a given app with its version number, release date in humanized form, and the changelog of this given release with the information provided by the publisher, this changelog accepts and renders markdown. Also refactored some component names and logic for maintainability reasons. - Demo gif: + Added a Releases tab to the app info page of installed marketplace apps. This tab will show all the released versions of a given app with its version number, release date in humanized form, and the changelog of this given release with the information provided by the publisher, this changelog accepts and renders markdown. Also refactored some component names and logic for maintainability reasons. + Demo gif:  - Create Team with a member list of usernames ([#25868](https://github.com/RocketChat/Rocket.Chat/pull/25868)) @@ -1012,19 +1302,19 @@ - Engagement Metrics - Phase 2 ([#25505](https://github.com/RocketChat/Rocket.Chat/pull/25505)) - Add the following new statistics (metrics): - - Total Broadcast rooms - - Total rooms with an active Livestream; - - Total triggered emails; - - Total subscription roles; - - Total User Roles; - - Total uncaught exceptions; - - `homeTitleChanged`: boolean value to indicate whether the `Layout_Home_Title` setting has been changed; - - `homeBodyChanged`: boolean value to indicate whether the `Layout_Home_Body` setting has been changed; - - `customCSSChanged`: boolean value to indicate whether the `theme-custom-css` setting has been changed; - - `onLogoutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_On_Logout` setting has been changed; - - `loggedOutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_Out` setting has been changed; - - `loggedInCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_In` setting has been changed; + Add the following new statistics (metrics): + - Total Broadcast rooms + - Total rooms with an active Livestream; + - Total triggered emails; + - Total subscription roles; + - Total User Roles; + - Total uncaught exceptions; + - `homeTitleChanged`: boolean value to indicate whether the `Layout_Home_Title` setting has been changed; + - `homeBodyChanged`: boolean value to indicate whether the `Layout_Home_Body` setting has been changed; + - `customCSSChanged`: boolean value to indicate whether the `theme-custom-css` setting has been changed; + - `onLogoutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_On_Logout` setting has been changed; + - `loggedOutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_Out` setting has been changed; + - `loggedInCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_In` setting has been changed; - `matrixBridgeEnabled`: boolean value to indicate whether the Matrix bridge has been enabled; - Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) @@ -1039,116 +1329,100 @@ - Marketplace new app details page ([#24711](https://github.com/RocketChat/Rocket.Chat/pull/24711)) - Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h) - - ## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u) - New tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component. - - Demo gif: -  - - ## [MKP12 - Header](https://app.clickup.com/t/25rhm0x) - Implemented a new header for the marketplaces app details page. - -Changed the size of the app name; - -Implemented the app description field on the header; - -Changed the "metadata" section of the header(The part with the version and author information) now it also shows the last time the app was updated; - -Created a chip that will show when an app is part of one or more bundles and inform which are the bundles; - -Implemented a tooltip for the bundle chips; - -Created a new button + data badge component to substitute the current App Status; - -Changed the title of the "purchase button". Now it shows different text based on the "purchase type" of the app; - -Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed; - -Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs; - - Demo gif: -  - - ## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4) - Delivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab. - Demo image: -  - - ## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1) - Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. - - Before: -  - - After -  - - ## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12) - Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back. - Edit: After some design reconsideration, the page title was changed to App Info. - Demo gif: -  - - ## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7) - Implemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR. - - ## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26) - Created an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the "open" carousel, hover highlight on the carousel preview and close on esc press. - Demo gif: + Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h) + + ## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u) + New tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component. + + Demo gif: +  + + ## [MKP12 - Header](https://app.clickup.com/t/25rhm0x) + Implemented a new header for the marketplaces app details page. + -Changed the size of the app name; + -Implemented the app description field on the header; + -Changed the "metadata" section of the header(The part with the version and author information) now it also shows the last time the app was updated; + -Created a chip that will show when an app is part of one or more bundles and inform which are the bundles; + -Implemented a tooltip for the bundle chips; + -Created a new button + data badge component to substitute the current App Status; + -Changed the title of the "purchase button". Now it shows different text based on the "purchase type" of the app; + -Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed; + -Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs; + + Demo gif: +  + + ## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4) + Delivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab. + Demo image: +  + + ## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1) + Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. + + Before: +  + + After +  + + ## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12) + Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back. + Edit: After some design reconsideration, the page title was changed to App Info. + Demo gif: +  + + ## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7) + Implemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR. + + ## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26) + Created an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the "open" carousel, hover highlight on the carousel preview and close on esc press. + Demo gif:  - Marketplace security tab app info page ([#25739](https://github.com/RocketChat/Rocket.Chat/pull/25739)) - Created a new security tab for installed apps that displays information related to the given app security policies, terms of services, and necessary permissions for the use of the app. - Demo gif: + Created a new security tab for installed apps that displays information related to the given app security policies, terms of services, and necessary permissions for the use of the app. + Demo gif:  - Matrix Federation UX improvements ([#25847](https://github.com/RocketChat/Rocket.Chat/pull/25847)) - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). - - - -  - In case you encounter any problems, or want to compare, temporarily it is possible to use the old version - + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + +  + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + <img width="556" alt="image" src="https://user-images.githubusercontent.com/5263975/162099800-15806953-f2f5-4905-a424-3f095076bc1d.png"> - New button for network outage ([#25499](https://github.com/RocketChat/Rocket.Chat/pull/25499) by [@amolghode1981](https://github.com/amolghode1981)) - When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable. + When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable. Network outage handling is handled in https://app.clickup.com/t/245c0d8 task. - New stats rewrite ([#25078](https://github.com/RocketChat/Rocket.Chat/pull/25078) by [@ostjen](https://github.com/ostjen)) - Add the following new statistics (**metrics**): - - - - Total users with TOTP enabled; - - - Total users with 2FA enabled; - - - Total pinned messages; - - - Total starred messages; - - - Total email messages; - - - Total rooms with at least one starred message; - - - Total rooms with at least one pinned message; - - - Total encrypted rooms; - - - Total link invitations; - - - Total email invitations; - - - Logo change; - - - Number of rooms inside teams; - - - Number of default (auto-join) rooms inside teams; - - - Number of users created through link invitation; - - - Number of users created through manual entry; - + Add the following new statistics (**metrics**): + + - Total users with TOTP enabled; + - Total users with 2FA enabled; + - Total pinned messages; + - Total starred messages; + - Total email messages; + - Total rooms with at least one starred message; + - Total rooms with at least one pinned message; + - Total encrypted rooms; + - Total link invitations; + - Total email invitations; + - Logo change; + - Number of rooms inside teams; + - Number of default (auto-join) rooms inside teams; + - Number of users created through link invitation; + - Number of users created through manual entry; - Number of imported users (by import type); - Star message, report and delete message events ([#25383](https://github.com/RocketChat/Rocket.Chat/pull/25383)) @@ -1168,19 +1442,16 @@ - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) - Earlier OTR room uses only 2 states, we need more states to support future features. - This adds more states for the OTR contextualBar. - - - - Expired - <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> - - - - Declined - <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> - - - - Error + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> + + - Declined + <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> + + - Error <img width="343" alt="Screen Shot 2022-04-20 at 13 55 26" src="https://user-images.githubusercontent.com/27704687/164283261-95e06d06-b0d0-402d-bccc-66596ff4dcd3.png"> - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) @@ -1195,19 +1466,15 @@ - Differ Voip calls from Incoming and Outgoing ([#25643](https://github.com/RocketChat/Rocket.Chat/pull/25643)) - Updated this column and its respective endpoints to support inbound/outfound call definitions + Updated this column and its respective endpoints to support inbound/outfound call definitions  - Expand the feature set of the new message rendering ([#25970](https://github.com/RocketChat/Rocket.Chat/pull/25970)) - - Everything inside a new package (`@rocket.chat/gazzodown`); - - - KaTeX support; - - - Highlighted Words support; - - - Emoji rendering expanded; - + - Everything inside a new package (`@rocket.chat/gazzodown`); + - KaTeX support; + - Highlighted Words support; + - Emoji rendering expanded; - Code rendering fixed - Fix multiple bugs with Matrix bridge ([#25318](https://github.com/RocketChat/Rocket.Chat/pull/25318)) @@ -1232,11 +1499,11 @@ - Refactor + unit tests for federation-v2 ([#25680](https://github.com/RocketChat/Rocket.Chat/pull/25680)) - The main goal for this PR is to add the ability to add tests in our current federation-v2 implementation. - In this PR, I've added only unit tests (80%), but the goal is to add other kinds of tests in the near future. - - Also, I've created a diagram to show how this refactor was done, and how is the structure of the code - + The main goal for this PR is to add the ability to add tests in our current federation-v2 implementation. + In this PR, I've added only unit tests (80%), but the goal is to add other kinds of tests in the near future. + + Also, I've created a diagram to show how this refactor was done, and how is the structure of the code +  - Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) @@ -1272,21 +1539,19 @@ - AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) - When filtering the analytics charts by department, data would not appear because the object: - ```js - { - value: "department-id", - label: "department-name" - } - ``` - was being used in the `departmentId` parameter. - - - - Before: -  - - - - After: + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: +  + + - After:  - AgentsPage pagination ([#25820](https://github.com/RocketChat/Rocket.Chat/pull/25820)) @@ -1301,13 +1566,13 @@ - Bump meteor-node-stubs to version 1.2.3 ([#25669](https://github.com/RocketChat/Rocket.Chat/pull/25669) by [@Sh0uld](https://github.com/Sh0uld)) - With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). + With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). For the issue in meteor see: https://github.com/meteor/meteor/issues/11974 - Change form body parameter charset to UTF-8 to fix issue #25456 ([#25673](https://github.com/RocketChat/Rocket.Chat/pull/25673) by [@divinespear](https://github.com/divinespear)) - since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. - + since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. +  - Change NPS Vote identifier + nps index to unique ([#25423](https://github.com/RocketChat/Rocket.Chat/pull/25423)) @@ -1318,26 +1583,23 @@ - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170) by [@amolghode1981](https://github.com/amolghode1981)) - Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online - unless agent explicitly logs off. - Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. - - 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off - in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. - - 2. Second reason is when computer goes in sleep mode. - - 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. - - Solution: - The idea is to detect the network disconnection and start the start the attempts to reconnect. - The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not - call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are - used. - - The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to - reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + When the server is disconnected, it should be indicated on the phone button. - Client-generated sort parameters in channel directory ([#25768](https://github.com/RocketChat/Rocket.Chat/pull/25768) by [@BenWiederhake](https://github.com/BenWiederhake)) @@ -1350,13 +1612,13 @@ - Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) - ## Before - - https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 - - - - ## After + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 - Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) @@ -1387,15 +1649,12 @@ - Fixing Network connectivity issues with SIP client. ([#25391](https://github.com/RocketChat/Rocket.Chat/pull/25391) by [@amolghode1981](https://github.com/amolghode1981)) - The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely. - This PR is expected to handle - - 1. Clearing call related UI when the network is disconnected or switched. - - 2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly - get disconnected after a while. This was due to the fact that the earlier socket disconnection caused the - removal of contact on asterisk. This should be fixed in this PR. - + The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely. + This PR is expected to handle + 1. Clearing call related UI when the network is disconnected or switched. + 2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly + get disconnected after a while. This was due to the fact that the earlier socket disconnection caused the + removal of contact on asterisk. This should be fixed in this PR. 3. This PR contains a lot of logs. This will be removed before the final merge. - FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) @@ -1412,10 +1671,10 @@ - Initial members value on Create Channel Modal ([#26000](https://github.com/RocketChat/Rocket.Chat/pull/26000)) - #### before -  - - #### after + #### before +  + + #### after  - Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) @@ -1432,8 +1691,8 @@ - Members selection field on creating team modal ([#25871](https://github.com/RocketChat/Rocket.Chat/pull/25871)) - - Fix: add members breaking when searching users - + - Fix: add members breaking when searching users +  - Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) @@ -1466,10 +1725,10 @@ - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - ### before - <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> - - ### after + ### before + <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> + + ### after <img width="300" alt="Screen Shot 2022-03-29 at 11 48 05" src="https://user-images.githubusercontent.com/27704687/160639208-3883a7b0-718a-4e9d-87b1-db960fe9bfcd.png"> - Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) @@ -1484,8 +1743,7 @@ - Remove 'total' text in admin info page ([#25638](https://github.com/RocketChat/Rocket.Chat/pull/25638)) - - Remove initial 'total' text from rooms and messages groups in the admin info page - + - Remove initial 'total' text from rooms and messages groups in the admin info page - Add 'total' before 'rooms' and 'messages' title on the same section. To use the new 'Total Rooms', was created a new key in the en.i18n.json file. - Remove duplicated icon bell when is thread main message ([#26051](https://github.com/RocketChat/Rocket.Chat/pull/26051)) @@ -1498,10 +1756,10 @@ - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) - ### before -  - - ### after + ### before +  + + ### after <img width="814" alt="Screenshot 2022-01-13 at 8 57 47 PM" src="https://user-images.githubusercontent.com/58601732/149359411-23e2430b-89e4-48b4-a3ad-65471d058551.png"> - Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) @@ -1512,27 +1770,20 @@ - Rooms' names turn lower case on CSV import ([#24612](https://github.com/RocketChat/Rocket.Chat/pull/24612)) - * Change 'Settings' import to not get cached configs - + * Change 'Settings' import to not get cached configs * Remove update `UI_Allow_room_names_with_special_chars` value - Sanitize customUserStatus and fix infinite loop ([#25449](https://github.com/RocketChat/Rocket.Chat/pull/25449)) - ### Additional improves: - - - usage of RHF to avoid unnecessary Add and Edit components separately and form validation - - - usage of `GenericTableV2` and some hooks to avoid unnecessary code - - - fix `IUserStatus` type - - - improves in UI design - - - improves **empty** and **loading** state - - - improves files structure - - [LOOP ERROR ATTACHMENT] + ### Additional improves: + - usage of RHF to avoid unnecessary Add and Edit components separately and form validation + - usage of `GenericTableV2` and some hooks to avoid unnecessary code + - fix `IUserStatus` type + - improves in UI design + - improves **empty** and **loading** state + - improves files structure + + [LOOP ERROR ATTACHMENT]  - Sanitize styles in message ([#25744](https://github.com/RocketChat/Rocket.Chat/pull/25744)) @@ -1571,18 +1822,18 @@ - Unnecessary padding on teams channels footer ([#25712](https://github.com/RocketChat/Rocket.Chat/pull/25712)) - #### before - <img width='320px' src='https://user-images.githubusercontent.com/27704687/171474795-2280a52c-b460-45f8-9b22-b61eb9d8075d.png' /> - - ### after + #### before + <img width='320px' src='https://user-images.githubusercontent.com/27704687/171474795-2280a52c-b460-45f8-9b22-b61eb9d8075d.png' /> + + ### after <img width='320px' src='https://user-images.githubusercontent.com/27704687/171475198-ee407192-95b2-44a4-9b55-374715078825.png' /> - Update chartjs usage to v3 ([#25873](https://github.com/RocketChat/Rocket.Chat/pull/25873)) - Update import from `csv-parse` ([#25872](https://github.com/RocketChat/Rocket.Chat/pull/25872)) - This PR updates the importing of `csv-parse` because the used method wasn't working anymore, we were receiving the following error: - + This PR updates the importing of `csv-parse` because the used method wasn't working anymore, we were receiving the following error: + `error: "this.csvParser is not a function"` - Update subscription on update team member ([#25855](https://github.com/RocketChat/Rocket.Chat/pull/25855)) @@ -1605,8 +1856,7 @@ - User avatar reseting and getting random image ([#25603](https://github.com/RocketChat/Rocket.Chat/pull/25603)) - - fixes user avatar not being saved after editing the user profile issue - + - fixes user avatar not being saved after editing the user profile issue - fixes user avatar not getting another user picture due to database deletion error - user status Offline misnamed as Invisible in Custom Status edit dropdown menu ([#24796](https://github.com/RocketChat/Rocket.Chat/pull/24796) by [@Kunalvrm555](https://github.com/Kunalvrm555)) @@ -1615,22 +1865,21 @@ - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) - ### before -  - - ### after + ### before +  + + ### after  - UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) - - Rewrites the component to TS - - - Fixes some visual issues - - ### before -  - - ### after + - Rewrites the component to TS + - Fixes some visual issues + + ### before +  + + ### after  - Users without the `view-other-user-info` permission can't use the `users.list` endpoint ([#26050](https://github.com/RocketChat/Rocket.Chat/pull/26050)) @@ -1645,26 +1894,23 @@ - VOIP CallContext snapshot infinite loop ([#25947](https://github.com/RocketChat/Rocket.Chat/pull/25947)) - The application was crashing due to an error on the `useCallerInfo()` hook. - The error was: -  -  - + The application was crashing due to an error on the `useCallerInfo()` hook. + The error was: +  +  + To prevent this issue to happen it was added a cached and out-of-scope snapshot variable to the hook using `useSyncExternalStore` - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230) by [@amolghode1981](https://github.com/amolghode1981)) - Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) - - It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. - - Solution: - - - 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. - - 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. - + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. - Voip endpoint permissions ([#25783](https://github.com/RocketChat/Rocket.Chat/pull/25783)) @@ -1713,8 +1959,8 @@ - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) - Not crash the whole application if something goes wrong in the MessageList component. - + Not crash the whole application if something goes wrong in the MessageList component. +  - Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) @@ -1769,8 +2015,8 @@ - Chore: Change Apps-Engine version source for info ([#26205](https://github.com/RocketChat/Rocket.Chat/pull/26205)) - Now that we're using `yarn`, the version stored in the `package.json` is no longer the resolved one, but it matches the input. This means that when we ran `yarn add @rocket.chat/apps-engine@alpha`, yarn saves `"alpha"` as the version of the package, while NPM added the resolved version for the tag, e.g. `"1.33.0-alpha.6507"`. This ends up breaking a few places where we need the Apps-Engine version for communication with the Marketplace. - + Now that we're using `yarn`, the version stored in the `package.json` is no longer the resolved one, but it matches the input. This means that when we ran `yarn add @rocket.chat/apps-engine@alpha`, yarn saves `"alpha"` as the version of the package, while NPM added the resolved version for the tag, e.g. `"1.33.0-alpha.6507"`. This ends up breaking a few places where we need the Apps-Engine version for communication with the Marketplace. + With this PR we change the source of that info so the problem doesn't happen anymore. - Chore: Change stats to daily ([#26113](https://github.com/RocketChat/Rocket.Chat/pull/26113)) @@ -1799,8 +2045,7 @@ - Chore: Convert Admin/OAuthApps to TS ([#25277](https://github.com/RocketChat/Rocket.Chat/pull/25277)) - - Converts Admin/OAuthApps to TS. - + - Converts Admin/OAuthApps to TS. - migrated forms to react-hook-form - Chore: Convert AdminSideBar to ts ([#25372](https://github.com/RocketChat/Rocket.Chat/pull/25372)) @@ -1865,10 +2110,10 @@ - Chore: convert marketplace price display component to use typescript ([#25504](https://github.com/RocketChat/Rocket.Chat/pull/25504)) - **Marketplace apps listing page** -  - - **Apps detail page** + **Marketplace apps listing page** +  + + **Apps detail page**  - Chore: Convert MemoizedSetting, Setting, Section ([#25572](https://github.com/RocketChat/Rocket.Chat/pull/25572)) @@ -1945,8 +2190,7 @@ - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) - - data and test-failure should be ignored - + - data and test-failure should be ignored - ensure scripts use cross-env - Chore: Fix CI ([#25797](https://github.com/RocketChat/Rocket.Chat/pull/25797)) @@ -2047,15 +2291,14 @@ - Chore: Notification Preferences to TS ([#25827](https://github.com/RocketChat/Rocket.Chat/pull/25827)) - - Notifications Preferences to TS. - + - Notifications Preferences to TS. - Fix broken save action. - Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) - Chore: Plan tag ([#26224](https://github.com/RocketChat/Rocket.Chat/pull/26224)) - Now we only have one plan tag for all plans \/ + Now we only have one plan tag for all plans \/  - Chore: Prune Messages contextualBar rewrite ([#25757](https://github.com/RocketChat/Rocket.Chat/pull/25757)) @@ -2094,15 +2337,12 @@ - Chore: Remove unused migrations ([#26102](https://github.com/RocketChat/Rocket.Chat/pull/26102)) - After giving it some thought: - - - - 234 through 240 are not going to be run anymore. Keeping them does not affect behavior of course, but this (removing) makes it easier to quickly glance at and understand what migrations are actually included in 5.x.y (especially in tag compare view or in general just checking the ref). - - - - Also changed the file name of 233 to be more explicit at what it does so to not confuse with actual "migrations" without having to open the file. - - + After giving it some thought: + + - 234 through 240 are not going to be run anymore. Keeping them does not affect behavior of course, but this (removing) makes it easier to quickly glance at and understand what migrations are actually included in 5.x.y (especially in tag compare view or in general just checking the ref). + + - Also changed the file name of 233 to be more explicit at what it does so to not confuse with actual "migrations" without having to open the file. + - The redirect to the documentation page (go.rocket....) is not yet set up, jfyi. - Chore: Reorder unreleased migrations ([#25508](https://github.com/RocketChat/Rocket.Chat/pull/25508)) @@ -2131,38 +2371,35 @@ - Chore: Rewrite im and dm endpoints to ts ([#25521](https://github.com/RocketChat/Rocket.Chat/pull/25521)) - - Endpoints rewritten to TS - - dm.create - - dm.delete - - dm.close - - dm.counters - - dm.files - - dm.history - - dm.members - - dm.messages - - dm.messages.others - - dm.list - - dm.list.everyone - - dm.open - - dm.setTopic - - im.create - - im.delete - - im.close - - im.counters - - im.files - - im.history - - im.members - - im.messages - - im.messages.others - - im.list - - im.list.everyone - - im.open - - im.setTopic - - - Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts` - - - Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts` - + - Endpoints rewritten to TS + - dm.create + - dm.delete + - dm.close + - dm.counters + - dm.files + - dm.history + - dm.members + - dm.messages + - dm.messages.others + - dm.list + - dm.list.everyone + - dm.open + - dm.setTopic + - im.create + - im.delete + - im.close + - im.counters + - im.files + - im.history + - im.members + - im.messages + - im.messages.others + - im.list + - im.list.everyone + - im.open + - im.setTopic + - Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts` + - Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts` - New types was added on `apps/meteor/app/api/server/api.d.ts` - Chore: Rewrite Jitsi Contextualbar to TS ([#25303](https://github.com/RocketChat/Rocket.Chat/pull/25303)) @@ -2173,7 +2410,7 @@ - Chore: Rewrite some Omnichannel files to TypeScript ([#25359](https://github.com/RocketChat/Rocket.Chat/pull/25359)) - apps/meteor/client/components/Omnichannel/modals/* + apps/meteor/client/components/Omnichannel/modals/* apps/meteor/client/components/Omnichannel/Tags.js - Chore: Room access validation may be called without user information ([#26086](https://github.com/RocketChat/Rocket.Chat/pull/26086)) @@ -2200,8 +2437,8 @@ - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - ``` - npx hygen package new test + ``` + npx hygen package new test ``` - Chore: Test for department screen ([#25696](https://github.com/RocketChat/Rocket.Chat/pull/25696)) @@ -2280,10 +2517,10 @@ - Chore: Watch for package changes ([#25910](https://github.com/RocketChat/Rocket.Chat/pull/25910)) - With the current `dev` pipeline, whenever we modify a package (e.g. `api-client`), we have to kill the meteor proccess and run `yarn dev` again in order for the changes to be compiled and the new output to be used by meteor. - - This has the drawback of taking a little longer to run the dev environment, since we can't cache a watched buid. In the other hand, it reduces the friction of modifying internal packages since we don't need to rebuild the project for changes to take effect. - + With the current `dev` pipeline, whenever we modify a package (e.g. `api-client`), we have to kill the meteor proccess and run `yarn dev` again in order for the changes to be compiled and the new output to be used by meteor. + + This has the drawback of taking a little longer to run the dev environment, since we can't cache a watched buid. In the other hand, it reduces the friction of modifying internal packages since we don't need to rebuild the project for changes to take effect. + This will enable us to move more things to separate packages without affecting the dev experience too much. - Chore(deps): Bump sharp from 0.30.4 to 0.30.6 ([#25719](https://github.com/RocketChat/Rocket.Chat/pull/25719) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -2328,13 +2565,13 @@ - Regression: Adjusted priority to run canned responses replace before new parser ([#26298](https://github.com/RocketChat/Rocket.Chat/pull/26298)) - Canned responses placeholders were not being replaced properly after we changed to the new md parser. - This fix changes the priority so that the canned responses replace logic runs before the parser, thus bringing back this functionality. - - Before: - <img width="329" alt="Screen Shot 2022-07-18 at 19 25 07" src="https://user-images.githubusercontent.com/6494543/179627632-754f1269-c0bd-498e-b09c-aeb2942fcae3.png"> - - After: + Canned responses placeholders were not being replaced properly after we changed to the new md parser. + This fix changes the priority so that the canned responses replace logic runs before the parser, thus bringing back this functionality. + + Before: + <img width="329" alt="Screen Shot 2022-07-18 at 19 25 07" src="https://user-images.githubusercontent.com/6494543/179627632-754f1269-c0bd-498e-b09c-aeb2942fcae3.png"> + + After: <img width="329" alt="Screen Shot 2022-07-18 at 19 26 09" src="https://user-images.githubusercontent.com/6494543/179627663-d0e558fb-2d99-4afe-aec9-14a5d3afae06.png"> - Regression: Align TypeScript version across workspaces ([#26184](https://github.com/RocketChat/Rocket.Chat/pull/26184)) @@ -2365,16 +2602,15 @@ - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - - Bump to 'next' the onboarding-ui package from fuselage. - + - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types - Regression: Burger menu showing arrow instead of burguer ([#26170](https://github.com/RocketChat/Rocket.Chat/pull/26170)) - Regression: Call toggle missing network disconnection state ([#26237](https://github.com/RocketChat/Rocket.Chat/pull/26237)) - This PR brings back the network disconnection state to the voip call toggle button - + This PR brings back the network disconnection state to the voip call toggle button +  - Regression: Calling info on VoipFooter when performing an outbound call ([#26138](https://github.com/RocketChat/Rocket.Chat/pull/26138)) @@ -2409,26 +2645,19 @@ - Regression: Contact manager edit/view not working ([#26155](https://github.com/RocketChat/Rocket.Chat/pull/26155)) - Basically, the Contact Center was working, but not the right way. This PR fixes: - - - Ability to select Contact Managers from dropdown - - - Ability to validate Contact Edits without requesting data a ton of times - - - Ability to remove Contact manager from a contact - - - Ability to see Contacts and Contact Managers on Contact View - - - Fix endpoints validation - + Basically, the Contact Center was working, but not the right way. This PR fixes: + - Ability to select Contact Managers from dropdown + - Ability to validate Contact Edits without requesting data a ton of times + - Ability to remove Contact manager from a contact + - Ability to see Contacts and Contact Managers on Contact View + - Fix endpoints validation - Add validators (ajv) to endpoint, thou not being used yet (since we hit a special endpoint) - Regression: Contact manager endpoint usage ([#26063](https://github.com/RocketChat/Rocket.Chat/pull/26063)) - Regression: Correct call ringtones ([#26111](https://github.com/RocketChat/Rocket.Chat/pull/26111)) - - outbound-call-ringing ringtone: Should be played when the outbound call is initiated and not yet established(Current implementation is playing the incoming-call ringtone) - + - outbound-call-ringing ringtone: Should be played when the outbound call is initiated and not yet established(Current implementation is playing the incoming-call ringtone) - call-ended ringtone: Should be played whenever a call ends. - Regression: Device management table missing device icon and ip text ellipsis ([#26255](https://github.com/RocketChat/Rocket.Chat/pull/26255)) @@ -2463,14 +2692,14 @@ - Regression: Fix blackscreen after app install ([#25950](https://github.com/RocketChat/Rocket.Chat/pull/25950)) - Fixed an error where the client screen would go black after installing an app. This was hapenning because the handleAppAddedOrUpdated function from the AppsProvider had a wrong type for the return of the getAppFromMarketplace function. - - Demo gifs: - - Before -  - - After: + Fixed an error where the client screen would go black after installing an app. This was hapenning because the handleAppAddedOrUpdated function from the AppsProvider had a wrong type for the return of the getAppFromMarketplace function. + + Demo gifs: + + Before +  + + After:  - Regression: Fix breaking omnichannel tests ([#26305](https://github.com/RocketChat/Rocket.Chat/pull/26305)) @@ -2507,20 +2736,20 @@ - Regression: Fix marketplace app apis visibility problem ([#26080](https://github.com/RocketChat/Rocket.Chat/pull/26080)) - Solved a problem that showed an unwanted zero in place of the APIs section for apps that weren't installed/did not have an APIs section. - Before: -  - - After(non installed app): -  - - After(installed app) + Solved a problem that showed an unwanted zero in place of the APIs section for apps that weren't installed/did not have an APIs section. + Before: +  + + After(non installed app): +  + + After(installed app)  - Regression: Fix marketplace releases tab crash bug ([#26162](https://github.com/RocketChat/Rocket.Chat/pull/26162)) - Fixed a bug where RC would crash because the marketplace releases tab was trying to display undefined data from manually installed apps. - Demo gif: + Fixed a bug where RC would crash because the marketplace releases tab was trying to display undefined data from manually installed apps. + Demo gif:  - Regression: Fix micro services ([#26054](https://github.com/RocketChat/Rocket.Chat/pull/26054)) @@ -2531,40 +2760,31 @@ - Regression: Fix Omnichannel not working after meteor update ([#26194](https://github.com/RocketChat/Rocket.Chat/pull/26194)) - Fixed things: - - - Omnichannel Directory - - - Omnichannel Current Chats - - - Auto Selection Algo - - - Load Balance Algo - - - Manual Selection Algo - - - Livechat New Conversations - - Other fixed things: - - - Warning on fields deprecation - - - Warning on "remove" deprecation - + Fixed things: + - Omnichannel Directory + - Omnichannel Current Chats + - Auto Selection Algo + - Load Balance Algo + - Manual Selection Algo + - Livechat New Conversations + + Other fixed things: + - Warning on fields deprecation + - Warning on "remove" deprecation - Remove findAndModify usage - Regression: Fix permissions page pagination ([#26304](https://github.com/RocketChat/Rocket.Chat/pull/26304)) - Regression: Fix rendered markdown styling on app info page details section ([#26093](https://github.com/RocketChat/Rocket.Chat/pull/26093)) - Fixed two styling problems on the AppDetails markdown. The first one was a misuse of flex and the second was the fact that the withRichContent flag was missing on the box that received the markdown. - Demo images: - Before: -  - - After: -  - + Fixed two styling problems on the AppDetails markdown. The first one was a misuse of flex and the second was the fact that the withRichContent flag was missing on the box that received the markdown. + Demo images: + Before: +  + + After: +  + Clickup task: https://app.clickup.com/t/2rwq0q7 - Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) @@ -2583,10 +2803,10 @@ - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) - The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. - - This temporarily switches to a fork of the matrix-appservice-bridge package. - + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). - Regression: Fix threads list ([#26052](https://github.com/RocketChat/Rocket.Chat/pull/26052)) @@ -2609,10 +2829,10 @@ - Regression: Livechat rooms not opening due to route desync ([#26209](https://github.com/RocketChat/Rocket.Chat/pull/26209)) - Due to route information only updating on `Tracker.afterFlush` (https://github.com/RocketChat/Rocket.Chat/pull/25990), we found out that calling the `tabBar.openUserInfo()` method at this point will cause a route change to the previous route instead of the current one, preventing livechat rooms from being opened. - - As a provisory solution, we're delaying the opening of the contextual bar, which then ensures that the route info is up to date. Although this solution works, we need to find a more reliable way of ensuring consistent route changes with up-to-date information. - + Due to route information only updating on `Tracker.afterFlush` (https://github.com/RocketChat/Rocket.Chat/pull/25990), we found out that calling the `tabBar.openUserInfo()` method at this point will cause a route change to the previous route instead of the current one, preventing livechat rooms from being opened. + + As a provisory solution, we're delaying the opening of the contextual bar, which then ensures that the route info is up to date. Although this solution works, we need to find a more reliable way of ensuring consistent route changes with up-to-date information. + ### I'm definitely open for better looking alternatives. Please leave a comment if you have a better solution to share. - Regression: Matrix Federation regressions ([#26283](https://github.com/RocketChat/Rocket.Chat/pull/26283)) @@ -2639,30 +2859,28 @@ - Regression: OTR with new React Messages ([#26179](https://github.com/RocketChat/Rocket.Chat/pull/26179)) - This PR solves 2 OTR issues with new react message components - - - - disable the server side message parser for OTR messages - - - adds the stopwatch icon for otr messages - - ### Before - <img width="548" alt="Screenshot 2022-07-08 at 12 58 08 AM" src="https://user-images.githubusercontent.com/58601732/177856565-54854a45-e82c-443b-b77d-66ec69d70522.png"> - - ### After + This PR solves 2 OTR issues with new react message components + + - disable the server side message parser for OTR messages + - adds the stopwatch icon for otr messages + + ### Before + <img width="548" alt="Screenshot 2022-07-08 at 12 58 08 AM" src="https://user-images.githubusercontent.com/58601732/177856565-54854a45-e82c-443b-b77d-66ec69d70522.png"> + + ### After <img width="549" alt="Screenshot 2022-07-08 at 12 55 08 AM" src="https://user-images.githubusercontent.com/58601732/177856597-67f7ca01-89ee-4ae9-bcd1-f4b6724db248.png"> - Regression: Parse outbound phone number removing * putting + char ([#26154](https://github.com/RocketChat/Rocket.Chat/pull/26154)) - Regression: Re-add view logs button ([#25876](https://github.com/RocketChat/Rocket.Chat/pull/25876)) - Re-added the view logs button to the appMenu component so that the user can go directly from the marketplace list of apps to the app info page with the logs tab already open. - Demo gif: + Re-added the view logs button to the appMenu component so that the user can go directly from the marketplace list of apps to the app info page with the logs tab already open. + Demo gif:  - Regression: Remove 4.0 version banner ([#26251](https://github.com/RocketChat/Rocket.Chat/pull/26251)) - Created a migration to disable and dismiss for all users the old 4.0 version banner. + Created a migration to disable and dismiss for all users the old 4.0 version banner. It happened when a new admin user has been added. - Regression: Remove alpha tag and fix initialization process ([#26248](https://github.com/RocketChat/Rocket.Chat/pull/26248)) @@ -2671,8 +2889,8 @@ - Regression: Removed CE watermark from VoipFooter ([#26239](https://github.com/RocketChat/Rocket.Chat/pull/26239)) - The objective of this change is to remove the CE watermark **only** during an active call. The CE watermark will be displayed normally in all other scenarios. Bellow you can see a demonstration of the expected behavior: - + The objective of this change is to remove the CE watermark **only** during an active call. The CE watermark will be displayed normally in all other scenarios. Bellow you can see a demonstration of the expected behavior: +  - Regression: Replace contact center icon ([#26216](https://github.com/RocketChat/Rocket.Chat/pull/26216)) @@ -2709,33 +2927,29 @@ - Regression: Sidebar icons spacing ([#26139](https://github.com/RocketChat/Rocket.Chat/pull/26139)) - - Fixed the sidebar icons ('display' and 'create new') spacing issue - - before: -  - - - - after: - + - Fixed the sidebar icons ('display' and 'create new') spacing issue + + before: +  + + + + after: +  - Regression: Special characters on phone number ([#26241](https://github.com/RocketChat/Rocket.Chat/pull/26241)) - PR Includes: - - - Keep focus on phone input of dial pad - - - Handle submit with "Enter" key - - - Remove mask and mandatory "+" char - + PR Includes: + - Keep focus on phone input of dial pad + - Handle submit with "Enter" key + - Remove mask and mandatory "+" char - Long press for "0"/"+" button - Regression: Subscription menu not appearing for non installed but subscribed apps ([#25627](https://github.com/RocketChat/Rocket.Chat/pull/25627)) - Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. - Demo gif: + Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. + Demo gif:  - Regression: TOTP Modal with new rest api package ([#25893](https://github.com/RocketChat/Rocket.Chat/pull/25893)) @@ -2764,22 +2978,19 @@ - Regression: Use fname instead real unique name for Voip ([#26319](https://github.com/RocketChat/Rocket.Chat/pull/26319)) - Affect: - - - Voip room header - - - Contacts table - + Affect: + - Voip room header + - Contacts table - Contact info - Regression: UserInfo/RoomInfo Menu ([#26252](https://github.com/RocketChat/Rocket.Chat/pull/26252)) - **note**: next fuselage's version needed - - #### before -  - - #### after + **note**: next fuselage's version needed + + #### before +  + + #### after  - Regression: Users on new sessions are forced to re-configure 2fa ([#26117](https://github.com/RocketChat/Rocket.Chat/pull/26117)) @@ -2976,7 +3187,7 @@ - Bump meteor-node-stubs to version 1.2.3 ([#25669](https://github.com/RocketChat/Rocket.Chat/pull/25669) by [@Sh0uld](https://github.com/Sh0uld)) - With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). + With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). For the issue in meteor see: https://github.com/meteor/meteor/issues/11974 - Fix prom-client new promise usage ([#25781](https://github.com/RocketChat/Rocket.Chat/pull/25781)) @@ -3021,8 +3232,8 @@ - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) - Experimental support for Matrix Federation with a Bridge - + Experimental support for Matrix Federation with a Bridge + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 - Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) @@ -3033,112 +3244,94 @@ - Marketplace new app details page ([#24711](https://github.com/RocketChat/Rocket.Chat/pull/24711)) - Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h) - - ## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u) - New tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component. - - Demo gif: -  - - ## [MKP12 - Header](https://app.clickup.com/t/25rhm0x) - Implemented a new header for the marketplaces app details page. - -Changed the size of the app name; - -Implemented the app description field on the header; - -Changed the "metadata" section of the header(The part with the version and author information) now it also shows the last time the app was updated; - -Created a chip that will show when an app is part of one or more bundles and inform which are the bundles; - -Implemented a tooltip for the bundle chips; - -Created a new button + data badge component to substitute the current App Status; - -Changed the title of the "purchase button". Now it shows different text based on the "purchase type" of the app; - -Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed; - -Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs; - - Demo gif: -  - - ## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4) - Delivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab. - Demo image: -  - - ## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1) - Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. - - Before: -  - - After -  - - ## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12) - Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back. - Edit: After some design reconsideration, the page title was changed to App Info. - Demo gif: -  - - ## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7) - Implemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR. - - ## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26) - Created an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the "open" carousel, hover highlight on the carousel preview and close on esc press. - Demo gif: + Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h) + + ## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u) + New tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component. + + Demo gif: +  + + ## [MKP12 - Header](https://app.clickup.com/t/25rhm0x) + Implemented a new header for the marketplaces app details page. + -Changed the size of the app name; + -Implemented the app description field on the header; + -Changed the "metadata" section of the header(The part with the version and author information) now it also shows the last time the app was updated; + -Created a chip that will show when an app is part of one or more bundles and inform which are the bundles; + -Implemented a tooltip for the bundle chips; + -Created a new button + data badge component to substitute the current App Status; + -Changed the title of the "purchase button". Now it shows different text based on the "purchase type" of the app; + -Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed; + -Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs; + + Demo gif: +  + + ## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4) + Delivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab. + Demo image: +  + + ## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1) + Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. + + Before: +  + + After +  + + ## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12) + Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back. + Edit: After some design reconsideration, the page title was changed to App Info. + Demo gif: +  + + ## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7) + Implemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR. + + ## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26) + Created an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the "open" carousel, hover highlight on the carousel preview and close on esc press. + Demo gif:  - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). - - - -  - In case you encounter any problems, or want to compare, temporarily it is possible to use the old version - + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + +  + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + <img width="556" alt="image" src="https://user-images.githubusercontent.com/5263975/162099800-15806953-f2f5-4905-a424-3f095076bc1d.png"> - New button for network outage ([#25499](https://github.com/RocketChat/Rocket.Chat/pull/25499) by [@amolghode1981](https://github.com/amolghode1981)) - When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable. + When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable. Network outage handling is handled in https://app.clickup.com/t/245c0d8 task. - New stats rewrite ([#25078](https://github.com/RocketChat/Rocket.Chat/pull/25078) by [@ostjen](https://github.com/ostjen)) - Add the following new statistics (**metrics**): - - - - Total users with TOTP enabled; - - - Total users with 2FA enabled; - - - Total pinned messages; - - - Total starred messages; - - - Total email messages; - - - Total rooms with at least one starred message; - - - Total rooms with at least one pinned message; - - - Total encrypted rooms; - - - Total link invitations; - - - Total email invitations; - - - Logo change; - - - Number of custom script lines; - - - Number of custom CSS lines; - - - Number of rooms inside teams; - - - Number of default (auto-join) rooms inside teams; - - - Number of users created through link invitation; - - - Number of users created through manual entry; - + Add the following new statistics (**metrics**): + + - Total users with TOTP enabled; + - Total users with 2FA enabled; + - Total pinned messages; + - Total starred messages; + - Total email messages; + - Total rooms with at least one starred message; + - Total rooms with at least one pinned message; + - Total encrypted rooms; + - Total link invitations; + - Total email invitations; + - Logo change; + - Number of custom script lines; + - Number of custom CSS lines; + - Number of rooms inside teams; + - Number of default (auto-join) rooms inside teams; + - Number of users created through link invitation; + - Number of users created through manual entry; - Number of imported users (by import type); - Star message, report and delete message events ([#25383](https://github.com/RocketChat/Rocket.Chat/pull/25383)) @@ -3152,19 +3345,16 @@ - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) - Earlier OTR room uses only 2 states, we need more states to support future features. - This adds more states for the OTR contextualBar. - - - - Expired - <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> - - - - Declined - <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> - - - - Error + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> + + - Declined + <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> + + - Error <img width="343" alt="Screen Shot 2022-04-20 at 13 55 26" src="https://user-images.githubusercontent.com/27704687/164283261-95e06d06-b0d0-402d-bccc-66596ff4dcd3.png"> - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) @@ -3210,27 +3400,25 @@ - AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) - When filtering the analytics charts by department, data would not appear because the object: - ```js - { - value: "department-id", - label: "department-name" - } - ``` - was being used in the `departmentId` parameter. - - - - Before: -  - - - - After: + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: +  + + - After:  - Change form body parameter charset to UTF-8 to fix issue #25456 ([#25673](https://github.com/RocketChat/Rocket.Chat/pull/25673) by [@divinespear](https://github.com/divinespear)) - since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. - + since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. +  - Change NPS Vote identifier + nps index to unique ([#25423](https://github.com/RocketChat/Rocket.Chat/pull/25423)) @@ -3241,26 +3429,23 @@ - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170) by [@amolghode1981](https://github.com/amolghode1981)) - Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online - unless agent explicitly logs off. - Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. - - 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off - in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. - - 2. Second reason is when computer goes in sleep mode. - - 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. - - Solution: - The idea is to detect the network disconnection and start the start the attempts to reconnect. - The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not - call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are - used. - - The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to - reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + When the server is disconnected, it should be indicated on the phone button. - Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) @@ -3269,13 +3454,13 @@ - Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) - ## Before - - https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 - - - - ## After + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 - Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) @@ -3290,15 +3475,12 @@ - Fixing Network connectivity issues with SIP client. ([#25391](https://github.com/RocketChat/Rocket.Chat/pull/25391) by [@amolghode1981](https://github.com/amolghode1981)) - The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely. - This PR is expected to handle - - 1. Clearing call related UI when the network is disconnected or switched. - - 2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly - get disconnected after a while. This was due to the fact that the earlier socket disconnection caused the - removal of contact on asterisk. This should be fixed in this PR. - + The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely. + This PR is expected to handle + 1. Clearing call related UI when the network is disconnected or switched. + 2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly + get disconnected after a while. This was due to the fact that the earlier socket disconnection caused the + removal of contact on asterisk. This should be fixed in this PR. 3. This PR contains a lot of logs. This will be removed before the final merge. - FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) @@ -3329,10 +3511,10 @@ - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - ### before - <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> - - ### after + ### before + <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> + + ### after <img width="300" alt="Screen Shot 2022-03-29 at 11 48 05" src="https://user-images.githubusercontent.com/27704687/160639208-3883a7b0-718a-4e9d-87b1-db960fe9bfcd.png"> - Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) @@ -3347,18 +3529,17 @@ - Remove 'total' text in admin info page ([#25638](https://github.com/RocketChat/Rocket.Chat/pull/25638)) - - Remove initial 'total' text from rooms and messages groups in the admin info page - + - Remove initial 'total' text from rooms and messages groups in the admin info page - Add 'total' before 'rooms' and 'messages' title on the same section. To use the new 'Total Rooms', was created a new key in the en.i18n.json file. - Removing user also removes them from Omni collections ([#25444](https://github.com/RocketChat/Rocket.Chat/pull/25444)) - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) - ### before -  - - ### after + ### before +  + + ### after <img width="814" alt="Screenshot 2022-01-13 at 8 57 47 PM" src="https://user-images.githubusercontent.com/58601732/149359411-23e2430b-89e4-48b4-a3ad-65471d058551.png"> - Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) @@ -3369,27 +3550,20 @@ - Rooms' names turn lower case on CSV import ([#24612](https://github.com/RocketChat/Rocket.Chat/pull/24612)) - * Change 'Settings' import to not get cached configs - + * Change 'Settings' import to not get cached configs * Remove update `UI_Allow_room_names_with_special_chars` value - Sanitize customUserStatus and fix infinite loop ([#25449](https://github.com/RocketChat/Rocket.Chat/pull/25449)) - ### Additional improves: - - - usage of RHF to avoid unnecessary Add and Edit components separately and form validation - - - usage of `GenericTableV2` and some hooks to avoid unnecessary code - - - fix `IUserStatus` type - - - improves in UI design - - - improves **empty** and **loading** state - - - improves files structure - - [LOOP ERROR ATTACHMENT] + ### Additional improves: + - usage of RHF to avoid unnecessary Add and Edit components separately and form validation + - usage of `GenericTableV2` and some hooks to avoid unnecessary code + - fix `IUserStatus` type + - improves in UI design + - improves **empty** and **loading** state + - improves files structure + + [LOOP ERROR ATTACHMENT]  - Settings listeners not receiving overwritten values from env vars ([#25448](https://github.com/RocketChat/Rocket.Chat/pull/25448)) @@ -3414,39 +3588,35 @@ - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) - ### before -  - - ### after + ### before +  + + ### after  - UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) - - Rewrites the component to TS - - - Fixes some visual issues - - ### before -  - - ### after + - Rewrites the component to TS + - Fixes some visual issues + + ### before +  + + ### after  - Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230) by [@amolghode1981](https://github.com/amolghode1981)) - Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) - - It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. - - Solution: - - - 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. - - 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. - + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. <details> @@ -3473,8 +3643,8 @@ - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) - Not crash the whole application if something goes wrong in the MessageList component. - + Not crash the whole application if something goes wrong in the MessageList component. +  - Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) @@ -3509,8 +3679,7 @@ - Chore: Convert Admin/OAuthApps to TS ([#25277](https://github.com/RocketChat/Rocket.Chat/pull/25277)) - - Converts Admin/OAuthApps to TS. - + - Converts Admin/OAuthApps to TS. - migrated forms to react-hook-form - Chore: Convert AdminSideBar to ts ([#25372](https://github.com/RocketChat/Rocket.Chat/pull/25372)) @@ -3545,10 +3714,10 @@ - Chore: convert marketplace price display component to use typescript ([#25504](https://github.com/RocketChat/Rocket.Chat/pull/25504)) - **Marketplace apps listing page** -  - - **Apps detail page** + **Marketplace apps listing page** +  + + **Apps detail page**  - Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) @@ -3585,8 +3754,7 @@ - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) - - data and test-failure should be ignored - + - data and test-failure should be ignored - ensure scripts use cross-env - Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) @@ -3655,38 +3823,35 @@ - Chore: Rewrite im and dm endpoints to ts ([#25521](https://github.com/RocketChat/Rocket.Chat/pull/25521)) - - Endpoints rewritten to TS - - dm.create - - dm.delete - - dm.close - - dm.counters - - dm.files - - dm.history - - dm.members - - dm.messages - - dm.messages.others - - dm.list - - dm.list.everyone - - dm.open - - dm.setTopic - - im.create - - im.delete - - im.close - - im.counters - - im.files - - im.history - - im.members - - im.messages - - im.messages.others - - im.list - - im.list.everyone - - im.open - - im.setTopic - - - Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts` - - - Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts` - + - Endpoints rewritten to TS + - dm.create + - dm.delete + - dm.close + - dm.counters + - dm.files + - dm.history + - dm.members + - dm.messages + - dm.messages.others + - dm.list + - dm.list.everyone + - dm.open + - dm.setTopic + - im.create + - im.delete + - im.close + - im.counters + - im.files + - im.history + - im.members + - im.messages + - im.messages.others + - im.list + - im.list.everyone + - im.open + - im.setTopic + - Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts` + - Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts` - New types was added on `apps/meteor/app/api/server/api.d.ts` - Chore: Rewrite Jitsi Contextualbar to TS ([#25303](https://github.com/RocketChat/Rocket.Chat/pull/25303)) @@ -3695,7 +3860,7 @@ - Chore: Rewrite some Omnichannel files to TypeScript ([#25359](https://github.com/RocketChat/Rocket.Chat/pull/25359)) - apps/meteor/client/components/Omnichannel/modals/* + apps/meteor/client/components/Omnichannel/modals/* apps/meteor/client/components/Omnichannel/Tags.js - Chore: solve yarn issues from env var ([#25468](https://github.com/RocketChat/Rocket.Chat/pull/25468)) @@ -3704,8 +3869,8 @@ - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - ``` - npx hygen package new test + ``` + npx hygen package new test ``` - Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) @@ -3768,8 +3933,7 @@ - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - - Bump to 'next' the onboarding-ui package from fuselage. - + - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types - Regression: Change logic to check if connection is online on unstable networks ([#25618](https://github.com/RocketChat/Rocket.Chat/pull/25618)) @@ -3814,10 +3978,10 @@ - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) - The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. - - This temporarily switches to a fork of the matrix-appservice-bridge package. - + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). - Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) @@ -3836,8 +4000,8 @@ - Regression: Subscription menu not appearing for non installed but subscribed apps ([#25627](https://github.com/RocketChat/Rocket.Chat/pull/25627)) - Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. - Demo gif: + Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. + Demo gif:  - Regression: Update settings groups description ([#25663](https://github.com/RocketChat/Rocket.Chat/pull/25663)) @@ -4063,21 +4227,21 @@ - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) - Experimental support for Matrix Federation with a Bridge - + Experimental support for Matrix Federation with a Bridge + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 - Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). - - - -  - In case you encounter any problems, or want to compare, temporarily it is possible to use the old version - + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + +  + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + <img width="556" alt="image" src="https://user-images.githubusercontent.com/5263975/162099800-15806953-f2f5-4905-a424-3f095076bc1d.png"> ### 🚀 Improvements @@ -4085,19 +4249,16 @@ - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) - Earlier OTR room uses only 2 states, we need more states to support future features. - This adds more states for the OTR contextualBar. - - - - Expired - <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> - - - - Declined - <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> - - - - Error + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> + + - Declined + <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> + + - Error <img width="343" alt="Screen Shot 2022-04-20 at 13 55 26" src="https://user-images.githubusercontent.com/27704687/164283261-95e06d06-b0d0-402d-bccc-66596ff4dcd3.png"> - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) @@ -4129,45 +4290,40 @@ - AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) - When filtering the analytics charts by department, data would not appear because the object: - ```js - { - value: "department-id", - label: "department-name" - } - ``` - was being used in the `departmentId` parameter. - - - - Before: -  - - - - After: + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: +  + + - After:  - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170) by [@amolghode1981](https://github.com/amolghode1981)) - Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online - unless agent explicitly logs off. - Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. - - 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off - in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. - - 2. Second reason is when computer goes in sleep mode. - - 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. - - Solution: - The idea is to detect the network disconnection and start the start the attempts to reconnect. - The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not - call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are - used. - - The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to - reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + When the server is disconnected, it should be indicated on the phone button. - Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) @@ -4176,13 +4332,13 @@ - Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) - ## Before - - https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 - - - - ## After + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 - Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) @@ -4205,10 +4361,10 @@ - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - ### before - <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> - - ### after + ### before + <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> + + ### after <img width="300" alt="Screen Shot 2022-03-29 at 11 48 05" src="https://user-images.githubusercontent.com/27704687/160639208-3883a7b0-718a-4e9d-87b1-db960fe9bfcd.png"> - Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) @@ -4221,10 +4377,10 @@ - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) - ### before -  - - ### after + ### before +  + + ### after <img width="814" alt="Screenshot 2022-01-13 at 8 57 47 PM" src="https://user-images.githubusercontent.com/58601732/149359411-23e2430b-89e4-48b4-a3ad-65471d058551.png"> - Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) @@ -4245,39 +4401,35 @@ - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) - ### before -  - - ### after + ### before +  + + ### after  - UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) - - Rewrites the component to TS - - - Fixes some visual issues - - ### before -  - - ### after + - Rewrites the component to TS + - Fixes some visual issues + + ### before +  + + ### after  - Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230) by [@amolghode1981](https://github.com/amolghode1981)) - Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) - - It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. - - Solution: - - - 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. - - 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. - + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. <details> @@ -4298,8 +4450,8 @@ - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) - Not crash the whole application if something goes wrong in the MessageList component. - + Not crash the whole application if something goes wrong in the MessageList component. +  - Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) @@ -4328,8 +4480,7 @@ - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) - - data and test-failure should be ignored - + - data and test-failure should be ignored - ensure scripts use cross-env - Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) @@ -4362,8 +4513,8 @@ - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - ``` - npx hygen package new test + ``` + npx hygen package new test ``` - Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) @@ -4400,8 +4551,7 @@ - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - - Bump to 'next' the onboarding-ui package from fuselage. - + - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types - Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) @@ -4434,10 +4584,10 @@ - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) - The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. - - This temporarily switches to a fork of the matrix-appservice-bridge package. - + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). - Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) @@ -4533,13 +4683,13 @@ - Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) - ## Before - - https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 - - - - ## After + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 ### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Contributors 😠@@ -4576,10 +4726,10 @@ - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) - ### before -  - - ### after + ### before +  + + ### after  ### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 @@ -4615,26 +4765,16 @@ - Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) - - Total of Canned response messages sent - - - Total of tags used - - - Last-Chatted Agent Preferred (enabled/disabled) - - - Assign new conversations to the contact manager (enabled/disabled) - - - How to handle Visitor Abandonment setting - - - Amount of chats placed on hold - - - VoIP Enabled - - - Amount of VoIP Calls - - - Amount of VoIP Extensions connected - - - Amount of Calls placed on hold (1x per call) - + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) - Fixed Session Aggregation type definitions - New omnichannel statistics and async statistics processing. ([#24749](https://github.com/RocketChat/Rocket.Chat/pull/24749)) @@ -4668,10 +4808,8 @@ - **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) - - Improve the CallProvider code; - - - Adjust the text case of the VoIP component on the FooterSidebar; - + - Improve the CallProvider code; + - Adjust the text case of the VoIP component on the FooterSidebar; - Fix the bad behavior with the changes in queue's name. - `PaginatedSelectFiltered` not handling changes ([#24732](https://github.com/RocketChat/Rocket.Chat/pull/24732)) @@ -4700,11 +4838,11 @@ - DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) - Before: - <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> - - - Now: + Before: + <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> + + + Now: <img width="611" alt="image" src="https://user-images.githubusercontent.com/40830821/159323594-10cf69a8-57dd-4e01-b4d3-31c92667a754.png"> - DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) @@ -4721,23 +4859,22 @@ - Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) - After resolving issue #24213 : - - + After resolving issue #24213 : + + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 - High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) - Remove infinity loop inside useVoipClient hook. - + Remove infinity loop inside useVoipClient hook. + #closes #24970 - Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) - LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) - - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. - Missing dependency on useEffect at CallProvider ([#24882](https://github.com/RocketChat/Rocket.Chat/pull/24882)) @@ -4764,27 +4901,26 @@ - Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) - Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. - This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. - + Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. + This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. + Also added a succes toast message after the successful deletion of room. - Revert AutoComplete ([#24812](https://github.com/RocketChat/Rocket.Chat/pull/24812)) - Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) - - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; - room message not load when is a new message ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) - When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. - - before: - https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 - - after: + When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. + + before: + https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 + + after: https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 - Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) @@ -4795,10 +4931,8 @@ - Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) - - Throw an error when trying to delete a role (User or Subscription role) that are still being used; - - - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - Fix "Users in Role" screen for custom roles. - Show call icon only when user has extension associated ([#24752](https://github.com/RocketChat/Rocket.Chat/pull/24752)) @@ -4879,15 +5013,14 @@ - Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - * Create a new test suite file under tests/end-to-end/api/livechat - * Create tests for the following endpoint: + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + ivechat/room.close - Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - - Create a new test suite file under tests/end-to-end/api/livechat - - - Create tests for the following endpoints: + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + livechat/visitor (create visitor, update visitor, add custom fields to visitors) - Chore: add some missing REST definitions ([#24925](https://github.com/RocketChat/Rocket.Chat/pull/24925) by [@gerzonc](https://github.com/gerzonc)) @@ -4940,10 +5073,8 @@ - Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) - - Stories from `ee/` included; - - - Differentiate root story kinds; - + - Stories from `ee/` included; + - Differentiate root story kinds; - Mocking of `ServerContext` via Storybook parameters. - Chore: Update Livechat ([#24754](https://github.com/RocketChat/Rocket.Chat/pull/24754)) @@ -4970,7 +5101,7 @@ - Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) -  +   - Regression: Error is raised when there's no Asterisk queue available yet ([#24980](https://github.com/RocketChat/Rocket.Chat/pull/24980) by [@amolghode1981](https://github.com/amolghode1981)) @@ -5066,18 +5197,18 @@ - High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) - Remove infinity loop inside useVoipClient hook. - + Remove infinity loop inside useVoipClient hook. + #closes #24970 - Multiple issues starting a new DM ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) - When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. - - before: - https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 - - after: + When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. + + before: + https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 + + after: https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 <details> @@ -5148,10 +5279,8 @@ - **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) - - Improve the CallProvider code; - - - Adjust the text case of the VoIP component on the FooterSidebar; - + - Improve the CallProvider code; + - Adjust the text case of the VoIP component on the FooterSidebar; - Fix the bad behavior with the changes in queue's name. - Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) @@ -5265,9 +5394,9 @@ - Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) - Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. - This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. - + Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. + This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. + Also added a succes toast message after the successful deletion of room. - Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) @@ -5332,38 +5461,25 @@ - Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) - Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. - Demo gif: + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif:  - VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102) by [@amolghode1981](https://github.com/amolghode1981)) - - Created VoipService to manage VoIP connections and PBX connection - - - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) - - - Created Basic interfaces to support new services and new model - - - Created Endpoints for management interfaces - - - Implemented asterisk connector on VoIP service - - - Created UI components to show calls incoming and to allow answering/rejecting calls - - - Added new settings to control call server/management server connection values - - - Added endpoints to associate Omnichannel Agents with PBX Extensions - - - Added support for event listening on server side, to get metadata about calls being received/ongoing - - - Created new pages to update settings & to see user-extension association - - - Created new page to see ongoing calls (and past calls) - - - Added support for remote hangup/hold on calls - - - Implemented call metrics calculation (hold time, waiting time, talk time) - + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) - Show a notificaiton when call is received ### 🚀 Improvements @@ -5373,27 +5489,26 @@ - Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) - The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. - Now, the same UI/UX is supported for chats opened from Current Chats list. - -  - - The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. - + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + +  + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. +  - Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) - The tooltips were missing on the action buttons of CR message composer. - -  - + The tooltips were missing on the action buttons of CR message composer. + +  + Users can now feel more encouraged to use these actions knowing what they are supposed to do. - Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) - - Add user to room on "Click to Join!" button press; - + - Add user to room on "Click to Join!" button press; - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). - Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) @@ -5408,10 +5523,10 @@ - CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) - ### before -  - - ### after + ### before +  + + ### after  - Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) @@ -5424,23 +5539,23 @@ - Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) - Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. - - Demo gif: -  - - Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. - Demo gif: + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: +  + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif:  - Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) - This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users - - ### before -  - - ### after + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before +  + + ### after  - Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) @@ -5449,17 +5564,15 @@ - Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) - - Delete some keys that aren't being used (eg: User_left_female). - - - Add new Teams' system messages: - - `added-user-to-team`: **added** @\user to this Team; - - `removed-user-from-team`: **removed** @\user from this Team; - - `user-converted-to-team`: **converted** #\room to a Team; - - `user-converted-to-channel`: **converted** #\room to a Channel; - - `user-removed-room-from-team`: **removed** @\user from this Team; - - `user-deleted-room-from-team`: **deleted** #\room from this Team; - - `user-added-room-to-team`: **deleted** #\room to this Team; - + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. ### 🛠Bug fixes @@ -5477,10 +5590,8 @@ - Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) - * Fix users selecting by fixing their _id - - * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone - + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone * Remove `disabled={usersCount === 0}` on user Tab - OAuth mismatch redirect_uri error ([#24450](https://github.com/RocketChat/Rocket.Chat/pull/24450)) @@ -5641,12 +5752,9 @@ - Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) - - Fix `Mentioned room not found` error when importing rooms from Slack; - - - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); - - - Fix incorrect message count on imported rooms; - + - Fix `Mentioned room not found` error when importing rooms from Slack; + - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); + - Fix incorrect message count on imported rooms; - Fix missing username on messages imported from Slack; - Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) @@ -5677,16 +5785,16 @@ - Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602) by [@amolghode1981](https://github.com/amolghode1981)) - The audio was not rendered because of re-rendering of react element based on - queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted - because after accepting call, queueCounter changes or a room gets created. - The audio element gets recreated. But VoIP user probably holds the old one. - The behaviour is not predictable when such case happens. If everything gets cleanly setup, - even if the audio element goes headless, it still continues to play the remote audio. - But in other cases, it is unreferenced the one on dom has its srcObject as null. - This causes no audio. - - This fix provides a way to re-initialise the rendering elements in VoIP user + The audio was not rendered because of re-rendering of react element based on + queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted + because after accepting call, queueCounter changes or a room gets created. + The audio element gets recreated. But VoIP user probably holds the old one. + The behaviour is not predictable when such case happens. If everything gets cleanly setup, + even if the audio element goes headless, it still continues to play the remote audio. + But in other cases, it is unreferenced the one on dom has its srcObject as null. + This causes no audio. + + This fix provides a way to re-initialise the rendering elements in VoIP user and calls this function on useEffect() if the re-render has happen. - Regression: Prevent button from losing state when rerendering ([#24648](https://github.com/RocketChat/Rocket.Chat/pull/24648)) @@ -5752,8 +5860,8 @@ - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) - Experimental support for Matrix Federation with a Bridge - + Experimental support for Matrix Federation with a Bridge + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 - E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -5766,19 +5874,19 @@ - Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) - Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. - Demo gif: + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif:  - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). - - - -  - In case you encounter any problems, or want to compare, temporarily it is possible to use the old version - + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + +  + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + <img width="556" alt="image" src="https://user-images.githubusercontent.com/5263975/162099800-15806953-f2f5-4905-a424-3f095076bc1d.png"> - Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -5793,32 +5901,19 @@ - VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102) by [@amolghode1981](https://github.com/amolghode1981)) - - Created VoipService to manage VoIP connections and PBX connection - - - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) - - - Created Basic interfaces to support new services and new model - - - Created Endpoints for management interfaces - - - Implemented asterisk connector on VoIP service - - - Created UI components to show calls incoming and to allow answering/rejecting calls - - - Added new settings to control call server/management server connection values - - - Added endpoints to associate Omnichannel Agents with PBX Extensions - - - Added support for event listening on server side, to get metadata about calls being received/ongoing - - - Created new pages to update settings & to see user-extension association - - - Created new page to see ongoing calls (and past calls) - - - Added support for remote hangup/hold on calls - - - Implemented call metrics calculation (hold time, waiting time, talk time) - + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) - Show a notificaiton when call is received ### 🚀 Improvements @@ -5830,46 +5925,42 @@ - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) - Earlier OTR room uses only 2 states, we need more states to support future features. - This adds more states for the OTR contextualBar. - - - - Expired - <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> - - - - Declined - <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> - - - - Error + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> + + - Declined + <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> + + - Error <img width="343" alt="Screen Shot 2022-04-20 at 13 55 26" src="https://user-images.githubusercontent.com/27704687/164283261-95e06d06-b0d0-402d-bccc-66596ff4dcd3.png"> - Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) - The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. - Now, the same UI/UX is supported for chats opened from Current Chats list. - -  - - The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. - + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + +  + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. +  - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) - Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) - The tooltips were missing on the action buttons of CR message composer. - -  - + The tooltips were missing on the action buttons of CR message composer. + +  + Users can now feel more encouraged to use these actions knowing what they are supposed to do. - Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) - - Add user to room on "Click to Join!" button press; - + - Add user to room on "Click to Join!" button press; - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). - Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) @@ -5880,26 +5971,16 @@ - Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) - - Total of Canned response messages sent - - - Total of tags used - - - Last-Chatted Agent Preferred (enabled/disabled) - - - Assign new conversations to the contact manager (enabled/disabled) - - - How to handle Visitor Abandonment setting - - - Amount of chats placed on hold - - - VoIP Enabled - - - Amount of VoIP Calls - - - Amount of VoIP Extensions connected - - - Amount of Calls placed on hold (1x per call) - + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) - Fixed Session Aggregation type definitions - ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -5912,10 +5993,10 @@ - CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) - ### before -  - - ### after + ### before +  + + ### after  - Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) @@ -5936,13 +6017,13 @@ - Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) - Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. - - Demo gif: -  - - Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. - Demo gif: + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: +  + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif:  - Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) @@ -5951,12 +6032,12 @@ - Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) - This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users - - ### before -  - - ### after + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before +  + + ### after  - Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) @@ -5965,17 +6046,15 @@ - Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) - - Delete some keys that aren't being used (eg: User_left_female). - - - Add new Teams' system messages: - - `added-user-to-team`: **added** @\user to this Team; - - `removed-user-from-team`: **removed** @\user from this Team; - - `user-converted-to-team`: **converted** #\room to a Team; - - `user-converted-to-channel`: **converted** #\room to a Channel; - - `user-removed-room-from-team`: **removed** @\user from this Team; - - `user-deleted-room-from-team`: **deleted** #\room from this Team; - - `user-added-room-to-team`: **deleted** #\room to this Team; - + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. - Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) @@ -6011,21 +6090,19 @@ - AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) - When filtering the analytics charts by department, data would not appear because the object: - ```js - { - value: "department-id", - label: "department-name" - } - ``` - was being used in the `departmentId` parameter. - - - - Before: -  - - - - After: + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: +  + + - After:  - API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) @@ -6036,26 +6113,23 @@ - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170) by [@amolghode1981](https://github.com/amolghode1981)) - Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online - unless agent explicitly logs off. - Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. - - 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off - in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. - - 2. Second reason is when computer goes in sleep mode. - - 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. - - Solution: - The idea is to detect the network disconnection and start the start the attempts to reconnect. - The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not - call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are - used. - - The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to - reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + When the server is disconnected, it should be indicated on the phone button. - Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) @@ -6070,11 +6144,11 @@ - DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) - Before: - <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> - - - Now: + Before: + <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> + + + Now: <img width="611" alt="image" src="https://user-images.githubusercontent.com/40830821/159323594-10cf69a8-57dd-4e01-b4d3-31c92667a754.png"> - DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) @@ -6095,9 +6169,9 @@ - Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) - After resolving issue #24213 : - - + After resolving issue #24213 : + + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 - Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) @@ -6114,16 +6188,13 @@ - Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) - * Fix users selecting by fixing their _id - - * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone - + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone * Remove `disabled={usersCount === 0}` on user Tab - LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) - - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. - LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) @@ -6152,10 +6223,10 @@ - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - ### before - <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> - - ### after + ### before + <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> + + ### after <img width="300" alt="Screen Shot 2022-03-29 at 11 48 05" src="https://user-images.githubusercontent.com/27704687/160639208-3883a7b0-718a-4e9d-87b1-db960fe9bfcd.png"> - Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) @@ -6172,10 +6243,10 @@ - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) - ### before -  - - ### after + ### before +  + + ### after <img width="814" alt="Screenshot 2022-01-13 at 8 57 47 PM" src="https://user-images.githubusercontent.com/58601732/149359411-23e2430b-89e4-48b4-a3ad-65471d058551.png"> - Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) @@ -6188,8 +6259,7 @@ - Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) - - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; - Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) @@ -6200,10 +6270,8 @@ - Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) - - Throw an error when trying to delete a role (User or Subscription role) that are still being used; - - - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - Fix "Users in Role" screen for custom roles. - Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) @@ -6236,31 +6304,27 @@ - UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) - - Rewrites the component to TS - - - Fixes some visual issues - - ### before -  - - ### after + - Rewrites the component to TS + - Fixes some visual issues + + ### before +  + + ### after  - Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230) by [@amolghode1981](https://github.com/amolghode1981)) - Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) - - It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. - - Solution: - - - 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. - - 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. - + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. - Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) @@ -6367,21 +6431,20 @@ - Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - * Create a new test suite file under tests/end-to-end/api/livechat - * Create tests for the following endpoint: + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + ivechat/room.close - Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - - Create a new test suite file under tests/end-to-end/api/livechat - - - Create tests for the following endpoints: + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + livechat/visitor (create visitor, update visitor, add custom fields to visitors) - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) - Not crash the whole application if something goes wrong in the MessageList component. - + Not crash the whole application if something goes wrong in the MessageList component. +  - Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) @@ -6474,8 +6537,7 @@ - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) - - data and test-failure should be ignored - + - data and test-failure should be ignored - ensure scripts use cross-env - Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) @@ -6538,18 +6600,16 @@ - Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) - - Stories from `ee/` included; - - - Differentiate root story kinds; - + - Stories from `ee/` included; + - Differentiate root story kinds; - Mocking of `ServerContext` via Storybook parameters. - Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - ``` - npx hygen package new test + ``` + npx hygen package new test ``` - Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) @@ -6632,8 +6692,7 @@ - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - - Bump to 'next' the onboarding-ui package from fuselage. - + - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types - Regression: Bunch of settings fixes for VoIP ([#24594](https://github.com/RocketChat/Rocket.Chat/pull/24594)) @@ -6646,7 +6705,7 @@ - Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) -  +   - Regression: Do not show toast on incoming voip calls ([#24619](https://github.com/RocketChat/Rocket.Chat/pull/24619)) @@ -6657,12 +6716,9 @@ - Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) - - Fix `Mentioned room not found` error when importing rooms from Slack; - - - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); - - - Fix incorrect message count on imported rooms; - + - Fix `Mentioned room not found` error when importing rooms from Slack; + - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); + - Fix incorrect message count on imported rooms; - Fix missing username on messages imported from Slack; - Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) @@ -6705,10 +6761,10 @@ - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) - The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. - - This temporarily switches to a fork of the matrix-appservice-bridge package. - + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). - Regression: Fix time fields and wrap up in Voip Room Contexual bar ([#24625](https://github.com/RocketChat/Rocket.Chat/pull/24625)) @@ -6733,16 +6789,16 @@ - Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602) by [@amolghode1981](https://github.com/amolghode1981)) - The audio was not rendered because of re-rendering of react element based on - queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted - because after accepting call, queueCounter changes or a room gets created. - The audio element gets recreated. But VoIP user probably holds the old one. - The behaviour is not predictable when such case happens. If everything gets cleanly setup, - even if the audio element goes headless, it still continues to play the remote audio. - But in other cases, it is unreferenced the one on dom has its srcObject as null. - This causes no audio. - - This fix provides a way to re-initialise the rendering elements in VoIP user + The audio was not rendered because of re-rendering of react element based on + queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted + because after accepting call, queueCounter changes or a room gets created. + The audio element gets recreated. But VoIP user probably holds the old one. + The behaviour is not predictable when such case happens. If everything gets cleanly setup, + even if the audio element goes headless, it still continues to play the remote audio. + But in other cases, it is unreferenced the one on dom has its srcObject as null. + This causes no audio. + + This fix provides a way to re-initialise the rendering elements in VoIP user and calls this function on useEffect() if the re-render has happen. - Regression: Prevent button from losing state when rerendering ([#24648](https://github.com/RocketChat/Rocket.Chat/pull/24648)) @@ -6878,8 +6934,8 @@ - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) - Experimental support for Matrix Federation with a Bridge - + Experimental support for Matrix Federation with a Bridge + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 - E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -6892,19 +6948,19 @@ - Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) - Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. - Demo gif: + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif:  - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). - - - -  - In case you encounter any problems, or want to compare, temporarily it is possible to use the old version - + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + +  + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + <img width="556" alt="image" src="https://user-images.githubusercontent.com/5263975/162099800-15806953-f2f5-4905-a424-3f095076bc1d.png"> - Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -6919,32 +6975,19 @@ - VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102) by [@amolghode1981](https://github.com/amolghode1981)) - - Created VoipService to manage VoIP connections and PBX connection - - - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) - - - Created Basic interfaces to support new services and new model - - - Created Endpoints for management interfaces - - - Implemented asterisk connector on VoIP service - - - Created UI components to show calls incoming and to allow answering/rejecting calls - - - Added new settings to control call server/management server connection values - - - Added endpoints to associate Omnichannel Agents with PBX Extensions - - - Added support for event listening on server side, to get metadata about calls being received/ongoing - - - Created new pages to update settings & to see user-extension association - - - Created new page to see ongoing calls (and past calls) - - - Added support for remote hangup/hold on calls - - - Implemented call metrics calculation (hold time, waiting time, talk time) - + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) - Show a notificaiton when call is received ### 🚀 Improvements @@ -6956,46 +6999,42 @@ - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) - Earlier OTR room uses only 2 states, we need more states to support future features. - This adds more states for the OTR contextualBar. - - - - Expired - <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> - - - - Declined - <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> - - - - Error + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + <img width="343" alt="Screen Shot 2022-04-20 at 13 55 52" src="https://user-images.githubusercontent.com/27704687/164283351-068756be-3419-4773-9d55-c9c1a72f5a19.png"> + + - Declined + <img width="343" alt='Screen Shot 2022-04-20 at 13 49 28' src='https://user-images.githubusercontent.com/27704687/164282312-fa3c6841-23d4-46e1-a8e9-80882a105d8c.png' /> + + - Error <img width="343" alt="Screen Shot 2022-04-20 at 13 55 26" src="https://user-images.githubusercontent.com/27704687/164283261-95e06d06-b0d0-402d-bccc-66596ff4dcd3.png"> - Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) - The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. - Now, the same UI/UX is supported for chats opened from Current Chats list. - -  - - The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. - + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + +  + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. +  - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) - Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) - The tooltips were missing on the action buttons of CR message composer. - -  - + The tooltips were missing on the action buttons of CR message composer. + +  + Users can now feel more encouraged to use these actions knowing what they are supposed to do. - Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) - - Add user to room on "Click to Join!" button press; - + - Add user to room on "Click to Join!" button press; - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). - Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) @@ -7006,26 +7045,16 @@ - Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) - - Total of Canned response messages sent - - - Total of tags used - - - Last-Chatted Agent Preferred (enabled/disabled) - - - Assign new conversations to the contact manager (enabled/disabled) - - - How to handle Visitor Abandonment setting - - - Amount of chats placed on hold - - - VoIP Enabled - - - Amount of VoIP Calls - - - Amount of VoIP Extensions connected - - - Amount of Calls placed on hold (1x per call) - + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) - Fixed Session Aggregation type definitions - ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) @@ -7038,10 +7067,10 @@ - CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) - ### before -  - - ### after + ### before +  + + ### after  - Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) @@ -7062,13 +7091,13 @@ - Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) - Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. - - Demo gif: -  - - Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. - Demo gif: + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: +  + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif:  - Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) @@ -7077,12 +7106,12 @@ - Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) - This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users - - ### before -  - - ### after + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before +  + + ### after  - Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) @@ -7091,17 +7120,15 @@ - Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) - - Delete some keys that aren't being used (eg: User_left_female). - - - Add new Teams' system messages: - - `added-user-to-team`: **added** @\user to this Team; - - `removed-user-from-team`: **removed** @\user from this Team; - - `user-converted-to-team`: **converted** #\room to a Team; - - `user-converted-to-channel`: **converted** #\room to a Channel; - - `user-removed-room-from-team`: **removed** @\user from this Team; - - `user-deleted-room-from-team`: **deleted** #\room from this Team; - - `user-added-room-to-team`: **deleted** #\room to this Team; - + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. - Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) @@ -7137,21 +7164,19 @@ - AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) - When filtering the analytics charts by department, data would not appear because the object: - ```js - { - value: "department-id", - label: "department-name" - } - ``` - was being used in the `departmentId` parameter. - - - - Before: -  - - - - After: + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: +  + + - After:  - API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) @@ -7162,26 +7187,23 @@ - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170) by [@amolghode1981](https://github.com/amolghode1981)) - Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online - unless agent explicitly logs off. - Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. - - 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off - in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. - - 2. Second reason is when computer goes in sleep mode. - - 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. - - Solution: - The idea is to detect the network disconnection and start the start the attempts to reconnect. - The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not - call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are - used. - - The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to - reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + When the server is disconnected, it should be indicated on the phone button. - Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) @@ -7196,11 +7218,11 @@ - DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) - Before: - <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> - - - Now: + Before: + <img width="267" alt="image" src="https://user-images.githubusercontent.com/40830821/159324037-b17e2492-e007-49fd-bfd1-f1d009301c44.png"> + + + Now: <img width="611" alt="image" src="https://user-images.githubusercontent.com/40830821/159323594-10cf69a8-57dd-4e01-b4d3-31c92667a754.png"> - DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) @@ -7221,9 +7243,9 @@ - Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) - After resolving issue #24213 : - - + After resolving issue #24213 : + + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 - Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) @@ -7240,16 +7262,13 @@ - Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) - * Fix users selecting by fixing their _id - - * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone - + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone * Remove `disabled={usersCount === 0}` on user Tab - LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) - - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. - LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) @@ -7278,10 +7297,10 @@ - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - ### before - <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> - - ### after + ### before + <img width="297" alt="Screen Shot 2022-03-29 at 13 35 56" src="https://user-images.githubusercontent.com/27704687/160661700-c2aebe05-a1be-4235-9d20-bce0b6e5fdb5.png"> + + ### after <img width="300" alt="Screen Shot 2022-03-29 at 11 48 05" src="https://user-images.githubusercontent.com/27704687/160639208-3883a7b0-718a-4e9d-87b1-db960fe9bfcd.png"> - Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) @@ -7298,10 +7317,10 @@ - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) - ### before -  - - ### after + ### before +  + + ### after <img width="814" alt="Screenshot 2022-01-13 at 8 57 47 PM" src="https://user-images.githubusercontent.com/58601732/149359411-23e2430b-89e4-48b4-a3ad-65471d058551.png"> - Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) @@ -7314,8 +7333,7 @@ - Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) - - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; - Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) @@ -7326,10 +7344,8 @@ - Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) - - Throw an error when trying to delete a role (User or Subscription role) that are still being used; - - - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - Fix "Users in Role" screen for custom roles. - Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) @@ -7362,31 +7378,27 @@ - UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) - - Rewrites the component to TS - - - Fixes some visual issues - - ### before -  - - ### after + - Rewrites the component to TS + - Fixes some visual issues + + ### before +  + + ### after  - Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230) by [@amolghode1981](https://github.com/amolghode1981)) - Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) - - It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. - - Solution: - - - 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. - - 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. - + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. - Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) @@ -7493,21 +7505,20 @@ - Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - * Create a new test suite file under tests/end-to-end/api/livechat - * Create tests for the following endpoint: + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + ivechat/room.close - Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - - Create a new test suite file under tests/end-to-end/api/livechat - - - Create tests for the following endpoints: + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + livechat/visitor (create visitor, update visitor, add custom fields to visitors) - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) - Not crash the whole application if something goes wrong in the MessageList component. - + Not crash the whole application if something goes wrong in the MessageList component. +  - Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) @@ -7600,8 +7611,7 @@ - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) - - data and test-failure should be ignored - + - data and test-failure should be ignored - ensure scripts use cross-env - Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) @@ -7664,18 +7674,16 @@ - Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) - - Stories from `ee/` included; - - - Differentiate root story kinds; - + - Stories from `ee/` included; + - Differentiate root story kinds; - Mocking of `ServerContext` via Storybook parameters. - Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - ``` - npx hygen package new test + ``` + npx hygen package new test ``` - Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) @@ -7758,8 +7766,7 @@ - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - - Bump to 'next' the onboarding-ui package from fuselage. - + - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types - Regression: Bunch of settings fixes for VoIP ([#24594](https://github.com/RocketChat/Rocket.Chat/pull/24594)) @@ -7772,7 +7779,7 @@ - Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) -  +   - Regression: Do not show toast on incoming voip calls ([#24619](https://github.com/RocketChat/Rocket.Chat/pull/24619)) @@ -7783,12 +7790,9 @@ - Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) - - Fix `Mentioned room not found` error when importing rooms from Slack; - - - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); - - - Fix incorrect message count on imported rooms; - + - Fix `Mentioned room not found` error when importing rooms from Slack; + - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); + - Fix incorrect message count on imported rooms; - Fix missing username on messages imported from Slack; - Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) @@ -7831,10 +7835,10 @@ - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) - The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. - - This temporarily switches to a fork of the matrix-appservice-bridge package. - + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). - Regression: Fix time fields and wrap up in Voip Room Contexual bar ([#24625](https://github.com/RocketChat/Rocket.Chat/pull/24625)) @@ -7859,16 +7863,16 @@ - Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602) by [@amolghode1981](https://github.com/amolghode1981)) - The audio was not rendered because of re-rendering of react element based on - queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted - because after accepting call, queueCounter changes or a room gets created. - The audio element gets recreated. But VoIP user probably holds the old one. - The behaviour is not predictable when such case happens. If everything gets cleanly setup, - even if the audio element goes headless, it still continues to play the remote audio. - But in other cases, it is unreferenced the one on dom has its srcObject as null. - This causes no audio. - - This fix provides a way to re-initialise the rendering elements in VoIP user + The audio was not rendered because of re-rendering of react element based on + queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted + because after accepting call, queueCounter changes or a room gets created. + The audio element gets recreated. But VoIP user probably holds the old one. + The behaviour is not predictable when such case happens. If everything gets cleanly setup, + even if the audio element goes headless, it still continues to play the remote audio. + But in other cases, it is unreferenced the one on dom has its srcObject as null. + This causes no audio. + + This fix provides a way to re-initialise the rendering elements in VoIP user and calls this function on useEffect() if the re-render has happen. - Regression: Prevent button from losing state when rerendering ([#24648](https://github.com/RocketChat/Rocket.Chat/pull/24648)) @@ -8104,16 +8108,16 @@ - App empty states component, category filter and empty states error variation implementations ([#23818](https://github.com/RocketChat/Rocket.Chat/pull/23818)) - Created and implemented the category filters component: - Demo gif: -  - - Created and implemented the empty states(States on fuselage) component: - Demo gif: -  - - Implemented a variations system for the empty states component and created a error message for network outage: - Demo gif: + Created and implemented the category filters component: + Demo gif: +  + + Created and implemented the empty states(States on fuselage) component: + Demo gif: +  + + Implemented a variations system for the empty states component and created a error message for network outage: + Demo gif:  - Apple Login ([#24060](https://github.com/RocketChat/Rocket.Chat/pull/24060)) @@ -8129,42 +8133,41 @@ - Admin page header buttons consistency ([#24168](https://github.com/RocketChat/Rocket.Chat/pull/24168)) - ### before -  -  -  -  -  - - - ### after -  -  -  -  + ### before +  +  +  +  +  + + + ### after +  +  +  +   - Importer text for CSV upload file format ([#23817](https://github.com/RocketChat/Rocket.Chat/pull/23817) by [@ostjen](https://github.com/ostjen)) - lib/Statistics improved and metrics collector ([#24177](https://github.com/RocketChat/Rocket.Chat/pull/24177) by [@ostjen](https://github.com/ostjen)) - - On `statistics` object the property `get` is an async function now. - - - We need to collect additional data of feature activation through the statistics collector. + - On `statistics` object the property `get` is an async function now. + - We need to collect additional data of feature activation through the statistics collector. - Some codes were splitted into another file just to organize. - Limit recent emojis to 27 ([#24210](https://github.com/RocketChat/Rocket.Chat/pull/24210)) - Limits the recent emoji list to a maximum of 3 rows instead of listing every emoji you've used so far. - + Limits the recent emoji list to a maximum of 3 rows instead of listing every emoji you've used so far. +  - Rewrite AddWebdavAccountModal to React Component ([#24070](https://github.com/RocketChat/Rocket.Chat/pull/24070)) - ### before -  - - ### after + ### before +  + + ### after  - Rewrite Omnichannel Queue Page to React ([#24176](https://github.com/RocketChat/Rocket.Chat/pull/24176)) @@ -8173,32 +8176,28 @@ - Rewrite roomNotFound to React Component ([#24044](https://github.com/RocketChat/Rocket.Chat/pull/24044)) - ### before -  -  - - ### after -  + ### before +  +  + + ### after +   - Setup Wizard Registration Flow ([#23676](https://github.com/RocketChat/Rocket.Chat/pull/23676)) - This pull request brings a few improvements in our setup wizard flow, the very first contact with a Rocket.Chat. Some of them: - - - A brand new visual design; - - - Form validation improves; - - - Allow users to navigate back to all steps; - - - Optimized steps to register your workspace or keep standalone. And many more! - - + This pull request brings a few improvements in our setup wizard flow, the very first contact with a Rocket.Chat. Some of them: + - A brand new visual design; + - Form validation improves; + - Allow users to navigate back to all steps; + - Optimized steps to register your workspace or keep standalone. And many more! + +  - Show Channel Icons on Room Header & Info panels ([#24239](https://github.com/RocketChat/Rocket.Chat/pull/24239)) - Updates Omnichannel Header & room Info component to render the source info + Updates Omnichannel Header & room Info component to render the source info Built on top of https://github.com/RocketChat/Rocket.Chat/pull/24237 - Throw 404 error in invalid endpoints ([#24053](https://github.com/RocketChat/Rocket.Chat/pull/24053)) @@ -8228,15 +8227,13 @@ - Apps Contextual Bar not carrying title and room information ([#24241](https://github.com/RocketChat/Rocket.Chat/pull/24241)) - Fixes: - - - - the app's name being rendered instead of the view's title, - - - the room's information (`IRoom`) wasn't being sent to the app when a `block action` happened - - Fixed behavior with correct view title and room information included in the block action event: - + Fixes: + + - the app's name being rendered instead of the view's title, + - the room's information (`IRoom`) wasn't being sent to the app when a `block action` happened + + Fixed behavior with correct view title and room information included in the block action event: + https://user-images.githubusercontent.com/733282/150420847-59bfcf8a-24a9-4dc5-8609-0d92dba38b70.mp4 - Avoid updating all rooms with visitor abandonment queries ([#24252](https://github.com/RocketChat/Rocket.Chat/pull/24252)) @@ -8249,26 +8246,24 @@ - Custom Emoji Image preview ([#24117](https://github.com/RocketChat/Rocket.Chat/pull/24117) by [@sidmohanty11](https://github.com/sidmohanty11)) - Before, - -  - - After, - -  - - also if any error, (for example - if we upload a video mp4 file) - + Before, + +  + + After, + +  + + also if any error, (for example - if we upload a video mp4 file) +  - Discussions not loading message history if not joined ([#24316](https://github.com/RocketChat/Rocket.Chat/pull/24316)) - Ensure Firefox 91 ESR support ([#24096](https://github.com/RocketChat/Rocket.Chat/pull/24096)) - It: - - - Adds `Firefox ESR` to `browserslist`; - + It: + - Adds `Firefox ESR` to `browserslist`; - Upgrades `@rocket.chat/fuselage-hooks` to overcome a bug related to Firefox implementation of `ResizeObserver` API. - Enter not working on modal's multi-line input ([#23981](https://github.com/RocketChat/Rocket.Chat/pull/23981)) @@ -8279,12 +8274,12 @@ - Filter ability for admin room checkboxes ([#23970](https://github.com/RocketChat/Rocket.Chat/pull/23970) by [@sidmohanty11](https://github.com/sidmohanty11)) - Now, - - https://user-images.githubusercontent.com/73601258/146380812-d3aa5561-64e1-4515-a639-3b6d87432ae4.mp4 - - Before, - + Now, + + https://user-images.githubusercontent.com/73601258/146380812-d3aa5561-64e1-4515-a639-3b6d87432ae4.mp4 + + Before, + https://user-images.githubusercontent.com/73601258/146385538-85a70fce-9974-40e0-8757-eda1a5d411b7.mp4 - Fixed broken links in setup wizard ([#24248](https://github.com/RocketChat/Rocket.Chat/pull/24248) by [@Himanshu664](https://github.com/Himanshu664)) @@ -8313,8 +8308,8 @@ - Solved Report Message Blank ([#24262](https://github.com/RocketChat/Rocket.Chat/pull/24262)) - After resolving issue #24261 : - + After resolving issue #24261 : + https://user-images.githubusercontent.com/53515714/150629459-5f0a9cf6-9b0e-417f-8fc1-44c810bd5428.mp4 - Wrong german translation for 2FA-Promt ([#24126](https://github.com/RocketChat/Rocket.Chat/pull/24126) by [@mbreslein-thd](https://github.com/mbreslein-thd)) @@ -8425,8 +8420,8 @@ - Regression: Remove extra call to `useOutsideClick` hook not following the function signature ([#24243](https://github.com/RocketChat/Rocket.Chat/pull/24243)) - It migrates `client/sidebar/header/actions/Search` component to TypeScript and mitigates a invalid call to `Array.prototype.every`: - + It migrates `client/sidebar/header/actions/Search` component to TypeScript and mitigates a invalid call to `Array.prototype.every`: +  - Regression: Standalone register path failing when saving data ([#24324](https://github.com/RocketChat/Rocket.Chat/pull/24324)) @@ -8581,10 +8576,8 @@ - Ensure Firefox 91 ESR support ([#24096](https://github.com/RocketChat/Rocket.Chat/pull/24096)) - It: - - - Adds `Firefox ESR` to `browserslist`; - + It: + - Adds `Firefox ESR` to `browserslist`; - Upgrades `@rocket.chat/fuselage-hooks` to overcome a bug related to Firefox implementation of `ResizeObserver` API. - Enter not working on modal's multi-line input ([#23981](https://github.com/RocketChat/Rocket.Chat/pull/23981)) @@ -8630,8 +8623,8 @@ - **APPS:** Allow apps to open contextual bar ([#23843](https://github.com/RocketChat/Rocket.Chat/pull/23843)) - Opens a contextual bar using app ui interactions (`CONTEXTUAL_BAR_OPEN`) - + Opens a contextual bar using app ui interactions (`CONTEXTUAL_BAR_OPEN`) + https://user-images.githubusercontent.com/733282/146704076-d2d115f2-6ca6-4ed0-b450-81be580889a4.mp4 - **APPS:** Allow Rocket.Chat Apps to register custom action buttons ([#23679](https://github.com/RocketChat/Rocket.Chat/pull/23679)) @@ -8642,7 +8635,7 @@ - **APPS:** Possibility to set room closer via Apps LivechatBridge.closeRoom ([#21025](https://github.com/RocketChat/Rocket.Chat/pull/21025)) - Add an optional param named `closer` into `LivechatBridge.closeRoom` so that it will be possible to close the room and send a close room message with the correct room closer. + Add an optional param named `closer` into `LivechatBridge.closeRoom` so that it will be possible to close the room and send a close room message with the correct room closer. If the param is not passed, use the room visitor as the room closer. - **EE:** Introduce fallback department support ([#23939](https://github.com/RocketChat/Rocket.Chat/pull/23939)) @@ -8662,8 +8655,7 @@ - Update "Message Erasure Type" setting's description ([#23879](https://github.com/RocketChat/Rocket.Chat/pull/23879)) - - Improves the "Message Erasure Type" setting's description by providing more details regarding the expected behavior of each option ("Keep Messages and User Name", "Delete All Messages" and "Remove link between user and messages"); - + - Improves the "Message Erasure Type" setting's description by providing more details regarding the expected behavior of each option ("Keep Messages and User Name", "Delete All Messages" and "Remove link between user and messages"); - Remove outdated translations (for this setting's description). - Webdav methods sanitization ([#23924](https://github.com/RocketChat/Rocket.Chat/pull/23924)) @@ -8705,23 +8697,23 @@ - Headers already sent error when user data download is disabled ([#23805](https://github.com/RocketChat/Rocket.Chat/pull/23805)) - When using the export message tool when trying to download the file using the link sent via email if the feature "Export User Data" is disabled an error was being thrown causing the request to halt. - - This is the error shown in the logs: - ``` - === UnHandledPromiseRejection === - Error [ERR_HTTP_HEADERS_SENT] [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client - at ServerResponse.setHeader (_http_outgoing.js:530:11) - at ServerResponse.res.setHeader (/app/bundle/programs/server/npm/node_modules/meteor/simple_json-routes/node_modules/connect/lib/patch.js:134:22) - at app/user-data-download/server/exportDownload.js:14:7 - at /app/bundle/programs/server/npm/node_modules/meteor/promise/node_modules/meteor-promise/fiber_pool.js:43:40 { - code: 'ERR_HTTP_HEADERS_SENT' - } - --------------------------------- - Errors like this can cause oplog processing errors. - Setting EXIT_UNHANDLEDPROMISEREJECTION will cause the process to exit allowing your service to automatically restart the process - Future node.js versions will automatically exit the process - ================================= + When using the export message tool when trying to download the file using the link sent via email if the feature "Export User Data" is disabled an error was being thrown causing the request to halt. + + This is the error shown in the logs: + ``` + === UnHandledPromiseRejection === + Error [ERR_HTTP_HEADERS_SENT] [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client + at ServerResponse.setHeader (_http_outgoing.js:530:11) + at ServerResponse.res.setHeader (/app/bundle/programs/server/npm/node_modules/meteor/simple_json-routes/node_modules/connect/lib/patch.js:134:22) + at app/user-data-download/server/exportDownload.js:14:7 + at /app/bundle/programs/server/npm/node_modules/meteor/promise/node_modules/meteor-promise/fiber_pool.js:43:40 { + code: 'ERR_HTTP_HEADERS_SENT' + } + --------------------------------- + Errors like this can cause oplog processing errors. + Setting EXIT_UNHANDLEDPROMISEREJECTION will cause the process to exit allowing your service to automatically restart the process + Future node.js versions will automatically exit the process + ================================= ``` - Jitsi call already ended ([#23904](https://github.com/RocketChat/Rocket.Chat/pull/23904) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) @@ -8732,23 +8724,23 @@ - Missing custom user status ellipsis ([#23831](https://github.com/RocketChat/Rocket.Chat/pull/23831)) - ### before -  - - ### after + ### before +  + + ### after  - Missing edit icon in sequential thread messages ([#23948](https://github.com/RocketChat/Rocket.Chat/pull/23948)) - ### before -  - - ### after + ### before +  + + ### after  - Modal keeps state if reset too fast. ([#23791](https://github.com/RocketChat/Rocket.Chat/pull/23791)) - ~Queued updates so the Modal has a chance to close.~ + ~Queued updates so the Modal has a chance to close.~ Used a random key to ensure modal doesn't keep it's state. - OTR not working ([#23973](https://github.com/RocketChat/Rocket.Chat/pull/23973)) @@ -8763,8 +8755,8 @@ - Segmentation fault on CentOS 7 due to outdated `sharp` ([#23796](https://github.com/RocketChat/Rocket.Chat/pull/23796)) - Upgrades `sharp` to avoid a segmentation fault on CentOS 7 during startup related to `sharp.node` being loaded via `process.dlopen()`. - + Upgrades `sharp` to avoid a segmentation fault on CentOS 7 during startup related to `sharp.node` being loaded via `process.dlopen()`. + Suggested as a fix for versions `4.0.x` and `4.1.x`. - teams.leave client usage ([#23959](https://github.com/RocketChat/Rocket.Chat/pull/23959)) @@ -8777,10 +8769,10 @@ - Wrong button for non trial apps ([#23861](https://github.com/RocketChat/Rocket.Chat/pull/23861)) - This PR solves a bug on the marketplace that was happening with WhatsApp where it was displaying a trial button even though it didn't have a free trial period. The new verification I've added checks if the app is subscription-based and then checks if it has 0 trial days in all of its tiers. If it does, it shows a subscribe button. If it doesn't, it displays a trial button. Also, I've exposed the itsEnterpriseOnly flag as an extra measure in the case of apps like Facebook Messenger that are enterprise-only and consequently should show the subscribe button. - Before: -  - After: + This PR solves a bug on the marketplace that was happening with WhatsApp where it was displaying a trial button even though it didn't have a free trial period. The new verification I've added checks if the app is subscription-based and then checks if it has 0 trial days in all of its tiers. If it does, it shows a subscribe button. If it doesn't, it displays a trial button. Also, I've exposed the itsEnterpriseOnly flag as an extra measure in the case of apps like Facebook Messenger that are enterprise-only and consequently should show the subscribe button. + Before: +  + After:  <details> @@ -8805,48 +8797,46 @@ - Chore: Centralize email validation functionality ([#23816](https://github.com/RocketChat/Rocket.Chat/pull/23816)) - - Create lib for validating emails - + - Create lib for validating emails - Modify places that validate emails to use the new central function - Chore: Change Menu props to accept next fuselage version ([#23839](https://github.com/RocketChat/Rocket.Chat/pull/23839)) - Chore: Create script to add new migrations ([#23822](https://github.com/RocketChat/Rocket.Chat/pull/23822)) - - Create NPM script to add new migrations - + - Create NPM script to add new migrations - TODO: Infer next migration number from file list - Chore: Deleted LivechatPageVisited ([#23993](https://github.com/RocketChat/Rocket.Chat/pull/23993) by [@ostjen](https://github.com/ostjen)) - Chore: Enable prefer-optional-chain ESLint rule for TypeScript files ([#23786](https://github.com/RocketChat/Rocket.Chat/pull/23786)) - > Code is bad. It rots. It requires periodic maintenance. It has bugs that need to be found. New features mean old code has to be adapted. - > The more code you have, the more places there are for bugs to hide. The longer checkouts or compiles take. The longer it takes a new employee to make sense of your system. If you have to refactor there's more stuff to move around. - > Furthermore, more code often means less flexibility and functionality. This is counter-intuitive, but a lot of times a simple, elegant solution is faster and more general than the plodding mess of code produced by a programmer of lesser talent. - > Code is produced by engineers. To make more code requires more engineers. Engineers have n^2 communication costs, and all that code they add to the system, while expanding its capability, also increases a whole basket of costs. - > You should do whatever possible to increase the productivity of individual programmers in terms of the expressive power of the code they write. Less code to do the same thing (and possibly better). Less programmers to hire. Less organizational communication costs. - - — <cite>[Rich Skrenta][1]</cite> - - Mixing two problem domains in code is prone to errors. In this small example - - ```ts - declare const y: { z: unknown } | undefined; - - const x = y && y.z; - ``` - - we're (1) checking the nullity of `y` and (2) attributing `y.z` to `x`, where (2) is _clearly_ the main problem we're solving with code. The optional chaining is a good technique to handle nullity as a mere implementation detail: - - ```ts - declare const y: { z: unknown } | undefined; - - const x = y?.z; - ``` - - Attributing `y.z` to `x` is more easily readable than the nullity check of `y`. - + > Code is bad. It rots. It requires periodic maintenance. It has bugs that need to be found. New features mean old code has to be adapted. + > The more code you have, the more places there are for bugs to hide. The longer checkouts or compiles take. The longer it takes a new employee to make sense of your system. If you have to refactor there's more stuff to move around. + > Furthermore, more code often means less flexibility and functionality. This is counter-intuitive, but a lot of times a simple, elegant solution is faster and more general than the plodding mess of code produced by a programmer of lesser talent. + > Code is produced by engineers. To make more code requires more engineers. Engineers have n^2 communication costs, and all that code they add to the system, while expanding its capability, also increases a whole basket of costs. + > You should do whatever possible to increase the productivity of individual programmers in terms of the expressive power of the code they write. Less code to do the same thing (and possibly better). Less programmers to hire. Less organizational communication costs. + + — <cite>[Rich Skrenta][1]</cite> + + Mixing two problem domains in code is prone to errors. In this small example + + ```ts + declare const y: { z: unknown } | undefined; + + const x = y && y.z; + ``` + + we're (1) checking the nullity of `y` and (2) attributing `y.z` to `x`, where (2) is _clearly_ the main problem we're solving with code. The optional chaining is a good technique to handle nullity as a mere implementation detail: + + ```ts + declare const y: { z: unknown } | undefined; + + const x = y?.z; + ``` + + Attributing `y.z` to `x` is more easily readable than the nullity check of `y`. + This PR aims to add `@typescript-eslint/prefer-optional-chain` rule to ESlint configuration at warning level. - Chore: Fix hasRole warning ([#23914](https://github.com/RocketChat/Rocket.Chat/pull/23914)) @@ -8889,8 +8879,8 @@ - Regression: Ensure room action buttons only appear inside menu ([#24035](https://github.com/RocketChat/Rocket.Chat/pull/24035)) - Currently, action buttons registered by apps to appear in the ROOM_ACTION context show in the first position of the list, but since they don't have an icon they are effectively invisible in the tab bar. - + Currently, action buttons registered by apps to appear in the ROOM_ACTION context show in the first position of the list, but since they don't have an icon they are effectively invisible in the tab bar. + Here we change the order configuration of the button so we make sure it only shows inside the room menu - Regression: Fix omnichannel empty source usage ([#24008](https://github.com/RocketChat/Rocket.Chat/pull/24008)) @@ -8993,18 +8983,18 @@ - Segmentation fault on CentOS 7 due to outdated `sharp` ([#23796](https://github.com/RocketChat/Rocket.Chat/pull/23796)) - Upgrades `sharp` to avoid a segmentation fault on CentOS 7 during startup related to `sharp.node` being loaded via `process.dlopen()`. - + Upgrades `sharp` to avoid a segmentation fault on CentOS 7 during startup related to `sharp.node` being loaded via `process.dlopen()`. + Suggested as a fix for versions `4.0.x` and `4.1.x`. - teams.removeMembers client usage ([#23857](https://github.com/RocketChat/Rocket.Chat/pull/23857)) - Wrong button for non trial apps ([#23861](https://github.com/RocketChat/Rocket.Chat/pull/23861)) - This PR solves a bug on the marketplace that was happening with WhatsApp where it was displaying a trial button even though it didn't have a free trial period. The new verification I've added checks if the app is subscription-based and then checks if it has 0 trial days in all of its tiers. If it does, it shows a subscribe button. If it doesn't, it displays a trial button. Also, I've exposed the itsEnterpriseOnly flag as an extra measure in the case of apps like Facebook Messenger that are enterprise-only and consequently should show the subscribe button. - Before: -  - After: + This PR solves a bug on the marketplace that was happening with WhatsApp where it was displaying a trial button even though it didn't have a free trial period. The new verification I've added checks if the app is subscription-based and then checks if it has 0 trial days in all of its tiers. If it does, it shows a subscribe button. If it doesn't, it displays a trial button. Also, I've exposed the itsEnterpriseOnly flag as an extra measure in the case of apps like Facebook Messenger that are enterprise-only and consequently should show the subscribe button. + Before: +  + After:  <details> @@ -9075,28 +9065,26 @@ - Engagement Dashboard ([#23547](https://github.com/RocketChat/Rocket.Chat/pull/23547)) - - Adds helpers `onToggledFeature` for server and client code to handle license activation/deactivation without server restart; - - - Replaces usage of `useEndpointData` with `useQuery` (from [React Query](https://react-query.tanstack.com/)); - + - Adds helpers `onToggledFeature` for server and client code to handle license activation/deactivation without server restart; + - Replaces usage of `useEndpointData` with `useQuery` (from [React Query](https://react-query.tanstack.com/)); - Introduces `view-engagement-dashboard` permission. - Improve the add user drop down for add a user in create channel modal for UserAutoCompleteMultiple ([#23766](https://github.com/RocketChat/Rocket.Chat/pull/23766) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Seeing only the name of the person you are not adding is not practical in my opinion because two people can have the same name. Moreover, you can't see the username of the person you want to add in the dropdown. So I changed that and created another selection of users to show the username as well. I made this change so that it would appear in the key place for creating a room and adding a user. - - Before: - - https://user-images.githubusercontent.com/45966964/115287805-faac8d00-a150-11eb-871f-147ab011ced0.mp4 - - - After: - + Seeing only the name of the person you are not adding is not practical in my opinion because two people can have the same name. Moreover, you can't see the username of the person you want to add in the dropdown. So I changed that and created another selection of users to show the username as well. I made this change so that it would appear in the key place for creating a room and adding a user. + + Before: + + https://user-images.githubusercontent.com/45966964/115287805-faac8d00-a150-11eb-871f-147ab011ced0.mp4 + + + After: + https://user-images.githubusercontent.com/45966964/115287664-d2249300-a150-11eb-8cf6-0e04730b425d.mp4 - MKP12 - New UI - Merge Apps and Marketplace Tabs and Content ([#23542](https://github.com/RocketChat/Rocket.Chat/pull/23542)) - Merged the Marketplace and Apps page into a single page with a tabs component that changes between Markeplace and installed apps. + Merged the Marketplace and Apps page into a single page with a tabs component that changes between Markeplace and installed apps.  - Re-naming department query param for Twilio ([#23725](https://github.com/RocketChat/Rocket.Chat/pull/23725)) @@ -9109,16 +9097,11 @@ - Stricter API types ([#23735](https://github.com/RocketChat/Rocket.Chat/pull/23735)) - It: - - - Adds stricter types for `API`; - - - Enables types for `urlParams`; - - - Removes mandatory passage of `undefined` payload on client; - - - Corrects some regressions; - + It: + - Adds stricter types for `API`; + - Enables types for `urlParams`; + - Removes mandatory passage of `undefined` payload on client; + - Corrects some regressions; - Reassures my belief in TypeScript supremacy. ### 🛠Bug fixes @@ -9128,14 +9111,12 @@ - **ENTERPRISE:** OAuth "Merge Roles" removes roles from users ([#23588](https://github.com/RocketChat/Rocket.Chat/pull/23588)) - - Fix OAuth "Merge Roles": the "Merge Roles" option now synchronize only the roles described in the "**Roles to Sync**" setting available in each Custom OAuth settings' group (instead of replacing users' roles by their OAuth roles); - + - Fix OAuth "Merge Roles": the "Merge Roles" option now synchronize only the roles described in the "**Roles to Sync**" setting available in each Custom OAuth settings' group (instead of replacing users' roles by their OAuth roles); - Fix "Merge Roles" and "Channel Mapping" not being performed/updated on OAuth login. - **ENTERPRISE:** Private rooms and discussions can't be audited ([#23673](https://github.com/RocketChat/Rocket.Chat/pull/23673)) - - Add Private rooms (groups) and Discussions to the Message Auditing (Channels) autocomplete; - + - Add Private rooms (groups) and Discussions to the Message Auditing (Channels) autocomplete; - Update "Channels" tab name to "Rooms". - **ENTERPRISE:** Replace all occurrences of a placeholder on string instead of just first one ([#23703](https://github.com/RocketChat/Rocket.Chat/pull/23703)) @@ -9148,10 +9129,10 @@ - Apps scheduler "losing" jobs after server restart ([#23566](https://github.com/RocketChat/Rocket.Chat/pull/23566)) - If a job is scheduled and the server restarted, said job won't be executed, giving the impression it's been lost. - - What happens is that the scheduler is only started when some app tries to schedule an app - if that happens, all jobs that are "late" will be executed; if that doesn't happen, no job will run. - + If a job is scheduled and the server restarted, said job won't be executed, giving the impression it's been lost. + + What happens is that the scheduler is only started when some app tries to schedule an app - if that happens, all jobs that are "late" will be executed; if that doesn't happen, no job will run. + This PR starts the apps scheduler right after all apps have been loaded - Autofocus on search input in admin ([#23738](https://github.com/RocketChat/Rocket.Chat/pull/23738)) @@ -9180,16 +9161,16 @@ - Notifications are not being filtered ([#23487](https://github.com/RocketChat/Rocket.Chat/pull/23487)) - - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; - - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); + - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; + - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); - Rename 'mobileNotifications' user's preference to 'pushNotifications'. - Omnichannel business hours page breaking navigation ([#23595](https://github.com/RocketChat/Rocket.Chat/pull/23595) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) - Omnichannel contact center navigation ([#23691](https://github.com/RocketChat/Rocket.Chat/pull/23691)) - Derives from: https://github.com/RocketChat/Rocket.Chat/pull/23656 - + Derives from: https://github.com/RocketChat/Rocket.Chat/pull/23656 + This PR includes a different approach to solving navigation problems following the same code structure and UI definitions of other "ActionButtons" components in Sidebar. - Omnichannel status being changed on page refresh ([#23587](https://github.com/RocketChat/Rocket.Chat/pull/23587)) @@ -9226,40 +9207,32 @@ - Chore: Mocha testing configuration ([#23706](https://github.com/RocketChat/Rocket.Chat/pull/23706)) - We've been writing integration tests for the REST API quite regularly, but we can't say the same for UI-related modules. This PR is based on the assumption that _improving the developer experience on writing tests_ would increase our coverage and promote the adoption even for newcomers. - - Here as summary of the proposal: - - - - Change Mocha configuration files: - - Add a base configuration (`.mocharc.base.json`); - - Rename the configuration for REST API tests (`mocha_end_to_end.opts.js -> .mocharc.api.js`); - - Add a configuration for client modules (`.mocharc.client.js`); - - Enable ESLint for them. - - - Add a Mocha test command exclusive for client modules (`npm run testunit-client`); - - - Enable fast watch mode: - - Configure `ts-node` to only transpile code (skip type checking); - - Define a list of files to be watched. - - - Configure `mocha` environment on ESLint only for test files (required when using Mocha's globals); - - - Adopt Chai as our assertion library: - - Unify the setup of Chai plugins (`chai-spies`, `chai-datetime`, `chai-dom`); - - Replace `assert` with `chai`; - - Replace `chai.expect` with `expect`. - - - Enable integration tests with React components: - - Enable JSX support on our default Babel configuration; + We've been writing integration tests for the REST API quite regularly, but we can't say the same for UI-related modules. This PR is based on the assumption that _improving the developer experience on writing tests_ would increase our coverage and promote the adoption even for newcomers. + + Here as summary of the proposal: + + - Change Mocha configuration files: + - Add a base configuration (`.mocharc.base.json`); + - Rename the configuration for REST API tests (`mocha_end_to_end.opts.js -> .mocharc.api.js`); + - Add a configuration for client modules (`.mocharc.client.js`); + - Enable ESLint for them. + - Add a Mocha test command exclusive for client modules (`npm run testunit-client`); + - Enable fast watch mode: + - Configure `ts-node` to only transpile code (skip type checking); + - Define a list of files to be watched. + - Configure `mocha` environment on ESLint only for test files (required when using Mocha's globals); + - Adopt Chai as our assertion library: + - Unify the setup of Chai plugins (`chai-spies`, `chai-datetime`, `chai-dom`); + - Replace `assert` with `chai`; + - Replace `chai.expect` with `expect`. + - Enable integration tests with React components: + - Enable JSX support on our default Babel configuration; - Adopt [testing library](https://testing-library.com/). - Chore: Rearrange module typings ([#23452](https://github.com/RocketChat/Rocket.Chat/pull/23452)) - - Move all external module declarations (definitions and augmentations) to `/definition/externals`; - - - ~Symlink some modules on `/definition/externals` to `/ee/server/services/definition/externals`~ Share types with `/ee/server/services`; - + - Move all external module declarations (definitions and augmentations) to `/definition/externals`; + - ~Symlink some modules on `/definition/externals` to `/ee/server/services/definition/externals`~ Share types with `/ee/server/services`; - Use TypeScript as server code entrypoint. - Chore: Remove duplicated 'name' key from rate limiter logs ([#23771](https://github.com/RocketChat/Rocket.Chat/pull/23771)) @@ -9374,8 +9347,8 @@ - Notifications are not being filtered ([#23487](https://github.com/RocketChat/Rocket.Chat/pull/23487)) - - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; - - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); + - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; + - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); - Rename 'mobileNotifications' user's preference to 'pushNotifications'. - Omnichannel status being changed on page refresh ([#23587](https://github.com/RocketChat/Rocket.Chat/pull/23587)) @@ -9441,19 +9414,19 @@ - Make Livechat Instructions setting multi-line ([#23515](https://github.com/RocketChat/Rocket.Chat/pull/23515)) - Since now we're supporting markdown text on this field (via this PR - https://github.com/RocketChat/Rocket.Chat.Livechat/pull/648), it would be nice to make this setting multiline so users can have more space to edit the text + Since now we're supporting markdown text on this field (via this PR - https://github.com/RocketChat/Rocket.Chat.Livechat/pull/648), it would be nice to make this setting multiline so users can have more space to edit the text  - optimized groups.listAll response time ([#22941](https://github.com/RocketChat/Rocket.Chat/pull/22941) by [@ostjen](https://github.com/ostjen)) - groups.listAll endpoint was having performance issues, specially when the total number of groups was high. This happened because the endpoint was loading all objects in memory then using splice to paginate, instead of paginating beforehand. - - Considering 70k groups, this was the performance improvement: - - before -  - - after + groups.listAll endpoint was having performance issues, specially when the total number of groups was high. This happened because the endpoint was loading all objects in memory then using splice to paginate, instead of paginating beforehand. + + Considering 70k groups, this was the performance improvement: + + before +  + + after  ### 🛠Bug fixes @@ -9461,8 +9434,7 @@ - **APPS:** Communication problem when updating and uninstalling apps in cluster ([#23418](https://github.com/RocketChat/Rocket.Chat/pull/23418)) - - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. - + - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. - Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state. - **ENTERPRISE:** Omnichannel agent is not leaving the room when a forwarded chat is queued ([#23404](https://github.com/RocketChat/Rocket.Chat/pull/23404)) @@ -9489,10 +9461,10 @@ - Markdown quote message style ([#23462](https://github.com/RocketChat/Rocket.Chat/pull/23462)) - Before: -  - - After: + Before: +  + + After:  - MONGO_OPTIONS being ignored for oplog connection ([#23314](https://github.com/RocketChat/Rocket.Chat/pull/23314) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) @@ -9511,8 +9483,8 @@ - Read only description in team creation ([#23213](https://github.com/RocketChat/Rocket.Chat/pull/23213)) -  - +  +  - resumeToken not working ([#23379](https://github.com/RocketChat/Rocket.Chat/pull/23379)) @@ -9521,8 +9493,7 @@ - SAML Users' roles being reset to default on login ([#23411](https://github.com/RocketChat/Rocket.Chat/pull/23411)) - - Remove `roles` field update on `insertOrUpdateSAMLUser` function; - + - Remove `roles` field update on `insertOrUpdateSAMLUser` function; - Add SAML `syncRoles` event; - Server crashing when Routing method is not available at start ([#23473](https://github.com/RocketChat/Rocket.Chat/pull/23473)) @@ -9603,14 +9574,14 @@ - Chore: Startup Time ([#23210](https://github.com/RocketChat/Rocket.Chat/pull/23210)) - The settings logic has been improved as a whole. - - All the logic to get the data from the env var was confusing. - - Setting default values was tricky to understand. - - Every time the server booted, all settings were updated and callbacks were called 2x or more (horrible for environments with multiple instances and generating a turbulent startup). - + The settings logic has been improved as a whole. + + All the logic to get the data from the env var was confusing. + + Setting default values was tricky to understand. + + Every time the server booted, all settings were updated and callbacks were called 2x or more (horrible for environments with multiple instances and generating a turbulent startup). + `Settings.get(......, callback);` was deprecated. We now have better methods for each case. - Chore: Update Apps-Engine version ([#23375](https://github.com/RocketChat/Rocket.Chat/pull/23375)) @@ -9633,10 +9604,10 @@ - Regression: Mail body contains `undefined` text ([#23552](https://github.com/RocketChat/Rocket.Chat/pull/23552)) - ### Before -  - - ### After + ### Before +  + + ### After  - Regression: Prevent settings from getting updated ([#23556](https://github.com/RocketChat/Rocket.Chat/pull/23556)) @@ -9727,8 +9698,7 @@ - SAML Users' roles being reset to default on login ([#23411](https://github.com/RocketChat/Rocket.Chat/pull/23411)) - - Remove `roles` field update on `insertOrUpdateSAMLUser` function; - + - Remove `roles` field update on `insertOrUpdateSAMLUser` function; - Add SAML `syncRoles` event; <details> @@ -9760,8 +9730,7 @@ - **APPS:** Communication problem when updating and uninstalling apps in cluster ([#23418](https://github.com/RocketChat/Rocket.Chat/pull/23418)) - - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. - + - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. - Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state. - Server crashing when Routing method is not available at start ([#23473](https://github.com/RocketChat/Rocket.Chat/pull/23473)) @@ -9888,20 +9857,18 @@ - **ENTERPRISE:** "Download CSV" button doesn't work in the Engagement Dashboard's Active Users section ([#23013](https://github.com/RocketChat/Rocket.Chat/pull/23013)) - - Fix "Download CSV" button in the Engagement Dashboard's Active Users section; - - - Add column headers to the CSV file downloaded from the Engagement Dashboard's Active Users section; - + - Fix "Download CSV" button in the Engagement Dashboard's Active Users section; + - Add column headers to the CSV file downloaded from the Engagement Dashboard's Active Users section; - Split the data in multiple CSV files. - **ENTERPRISE:** CSV file downloaded in the Engagement Dashboard's New Users section contains undefined data ([#23014](https://github.com/RocketChat/Rocket.Chat/pull/23014)) - - Fix CSV file downloaded in the Engagement Dashboard's New Users section; + - Fix CSV file downloaded in the Engagement Dashboard's New Users section; - Add column headers to the CSV file downloaded from the Engagement Dashboard's New Users section. - **ENTERPRISE:** Missing headers in CSV files downloaded from the Engagement Dashboard ([#23223](https://github.com/RocketChat/Rocket.Chat/pull/23223)) - - Add headers to all CSV files downloaded from the "Messages" and "Channels" tabs from the Engagement Dashboard; + - Add headers to all CSV files downloaded from the "Messages" and "Channels" tabs from the Engagement Dashboard; - Add headers to the CSV file downloaded from the "Users by time of day" section (in the "Users" tab). - LDAP Refactoring ([#23171](https://github.com/RocketChat/Rocket.Chat/pull/23171)) @@ -9916,24 +9883,17 @@ - Remove deprecated endpoints ([#23162](https://github.com/RocketChat/Rocket.Chat/pull/23162)) - The following REST endpoints were removed: - - - - `/api/v1/emoji-custom` - - - `/api/v1/info` - - - `/api/v1/permissions` - - - `/api/v1/permissions.list` - - The following Real time API Methods were removed: - - - - `getFullUserData` - - - `getServerInfo` - + The following REST endpoints were removed: + + - `/api/v1/emoji-custom` + - `/api/v1/info` + - `/api/v1/permissions` + - `/api/v1/permissions.list` + + The following Real time API Methods were removed: + + - `getFullUserData` + - `getServerInfo` - `livechat:saveOfficeHours` - Remove Google Vision features ([#23160](https://github.com/RocketChat/Rocket.Chat/pull/23160)) @@ -9942,8 +9902,8 @@ - Remove old migrations up to version 2.4.14 ([#23277](https://github.com/RocketChat/Rocket.Chat/pull/23277)) - To update to version 4.0.0 you'll need to be running at least version 3.0.0, otherwise you might loose some database migrations which might have unexpected effects. - + To update to version 4.0.0 you'll need to be running at least version 3.0.0, otherwise you might loose some database migrations which might have unexpected effects. + This aims to clean up the code, since upgrades jumping 2 major versions are too risky and hard to maintain, we'll keep only migration from that last major (in this case 3.x). - Remove patch info from endpoint /api/info for non-logged in users ([#16050](https://github.com/RocketChat/Rocket.Chat/pull/16050)) @@ -9952,18 +9912,18 @@ - Stop sending audio notifications via stream ([#23108](https://github.com/RocketChat/Rocket.Chat/pull/23108)) - Remove audio preferences and make them tied to desktop notification preferences. - + Remove audio preferences and make them tied to desktop notification preferences. + TL;DR: new message sounds will play only if you receive a desktop notification. you'll still be able to chose to not play any sound though - Webhook will fail if user is not part of the channel ([#23310](https://github.com/RocketChat/Rocket.Chat/pull/23310)) - Remove deprecated behavior added by https://github.com/RocketChat/Rocket.Chat/pull/18024 that accepts webhook integrations sending messages even if the user is not part of the channel. - - Starting from 4.0.0 the webhook request will fail with `error-not-allowed` error: - - ``` - {"success":false,"error":"error-not-allowed"} + Remove deprecated behavior added by https://github.com/RocketChat/Rocket.Chat/pull/18024 that accepts webhook integrations sending messages even if the user is not part of the channel. + + Starting from 4.0.0 the webhook request will fail with `error-not-allowed` error: + + ``` + {"success":false,"error":"error-not-allowed"} ``` ### 🎉 New features @@ -9981,26 +9941,23 @@ - Seats Cap ([#23017](https://github.com/RocketChat/Rocket.Chat/pull/23017) by [@g-thome](https://github.com/g-thome)) - - Adding New Members - - Awareness of seats usage while adding new members - - Seats Cap about to be reached - - Seats Cap reached - - Request more seats - - - Warning Admins - - System telling admins max seats are about to exceed - - System telling admins max seats were exceed - - Metric on Info Page - - Request more seats - - - Warning Members - - Invite link - - Block creating new invite links - - Block existing invite links (feedback on register process) - - Register to Workspaces - - - Emails - - System telling admins max seats are about to exceed + - Adding New Members + - Awareness of seats usage while adding new members + - Seats Cap about to be reached + - Seats Cap reached + - Request more seats + - Warning Admins + - System telling admins max seats are about to exceed + - System telling admins max seats were exceed + - Metric on Info Page + - Request more seats + - Warning Members + - Invite link + - Block creating new invite links + - Block existing invite links (feedback on register process) + - Register to Workspaces + - Emails + - System telling admins max seats are about to exceed - System telling admins max seats were exceed ### 🚀 Improvements @@ -10008,10 +9965,10 @@ - **APPS:** New storage strategy for Apps-Engine file packages ([#22657](https://github.com/RocketChat/Rocket.Chat/pull/22657)) - This is an enabler for our initiative to support NPM packages in the Apps-Engine. - - Currently, the packages (zip files) for Rocket.Chat Apps are stored as a base64 encoded string in a document in the database, which constrains us due to the size limit of a document in MongoDB (16Mb). - + This is an enabler for our initiative to support NPM packages in the Apps-Engine. + + Currently, the packages (zip files) for Rocket.Chat Apps are stored as a base64 encoded string in a document in the database, which constrains us due to the size limit of a document in MongoDB (16Mb). + When we allow apps to include NPM packages, the size of the App package itself will be potentially _very large_ (I'm looking at you `node_modules`). Thus we'll be changing the strategy to store apps either with GridFS or the host's File System itself. - **APPS:** Return task ids when using the scheduler api ([#23023](https://github.com/RocketChat/Rocket.Chat/pull/23023)) @@ -10051,9 +10008,9 @@ - "Read Only" and "Allow Reacting" system messages are missing in rooms ([#23037](https://github.com/RocketChat/Rocket.Chat/pull/23037) by [@ostjen](https://github.com/ostjen)) - - Add system message to notify changes on the **"Read Only"** setting; - - Add system message to notify changes on the **"Allow Reacting"** setting; - - Fix "Allow Reacting" setting's description (updated from "Only authorized users can write new messages" to "Only authorized users can react to messages"). + - Add system message to notify changes on the **"Read Only"** setting; + - Add system message to notify changes on the **"Allow Reacting"** setting; + - Fix "Allow Reacting" setting's description (updated from "Only authorized users can write new messages" to "Only authorized users can react to messages").  - Add check before placing chat on-hold to confirm that contact sent last message ([#23053](https://github.com/RocketChat/Rocket.Chat/pull/23053)) @@ -10068,9 +10025,9 @@ - Inaccurate use of 'Mobile notifications' instead of 'Push notifications' in i18n strings ([#22978](https://github.com/RocketChat/Rocket.Chat/pull/22978) by [@ostjen](https://github.com/ostjen)) - - Fix inaccurate use of 'Mobile notifications' (which is misleading in German) by 'Push notifications'; - - Update `'Notification_Mobile_Default_For'` key to `'Notification_Push_Default_For'` (and text to 'Send Push Notifications For' for English Language); - - Update `'Accounts_Default_User_Preferences_mobileNotifications'` key to `'Accounts_Default_User_Preferences_pushNotifications'`; + - Fix inaccurate use of 'Mobile notifications' (which is misleading in German) by 'Push notifications'; + - Update `'Notification_Mobile_Default_For'` key to `'Notification_Push_Default_For'` (and text to 'Send Push Notifications For' for English Language); + - Update `'Accounts_Default_User_Preferences_mobileNotifications'` key to `'Accounts_Default_User_Preferences_pushNotifications'`; - Update `'Mobile_Notifications_Default_Alert'` key to `'Mobile_Push_Notifications_Default_Alert'`; - Logging out from other clients ([#23276](https://github.com/RocketChat/Rocket.Chat/pull/23276)) @@ -10079,7 +10036,7 @@ - Modals is cutting pixels of the content ([#23243](https://github.com/RocketChat/Rocket.Chat/pull/23243)) - Fuselage Dependency: [543](https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/543) + Fuselage Dependency: [543](https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/543)  - Omnichannel On hold chats being forwarded to offline agents ([#23185](https://github.com/RocketChat/Rocket.Chat/pull/23185)) @@ -10088,15 +10045,15 @@ - Prevent users to edit an existing role when adding a new one with the same name used before. ([#22407](https://github.com/RocketChat/Rocket.Chat/pull/22407) by [@lucassartor](https://github.com/lucassartor)) - ### before -  - - ### after + ### before +  + + ### after  - Remove doubled "Canned Responses" strings ([#23056](https://github.com/RocketChat/Rocket.Chat/pull/23056)) - - Remove doubled canned response setting introduced in #22703 (by setting id change); + - Remove doubled canned response setting introduced in #22703 (by setting id change); - Update "Canned Responses" keys to "Canned_Responses". - Remove margin from quote inside quote ([#21779](https://github.com/RocketChat/Rocket.Chat/pull/21779)) @@ -10107,21 +10064,16 @@ - Sidebar not closing when clicking in Home or Directory on mobile view ([#23218](https://github.com/RocketChat/Rocket.Chat/pull/23218)) - ### Additional fixed - - - Merge Burger menu components into a single component - - - Show a badge with no-read messages in the Burger Button: -  - + ### Additional fixed + - Merge Burger menu components into a single component + - Show a badge with no-read messages in the Burger Button: +  - remove useSidebarClose hook - Stop queue when Omnichannel is disabled or the routing method does not support it ([#23261](https://github.com/RocketChat/Rocket.Chat/pull/23261)) - - Add missing key logs - - - Stop queue (and logs) when livechat is disabled or when routing method does not support queue - + - Add missing key logs + - Stop queue (and logs) when livechat is disabled or when routing method does not support queue - Stop ignoring offline bot agents from delegation (previously, if a bot was offline, even with "Assign new conversations to bot agent" enabled, bot will be ignored and chat will be left in limbo (since bot was assigned, but offline). - Toolbox click not working on Safari(iOS) ([#23244](https://github.com/RocketChat/Rocket.Chat/pull/23244)) @@ -10228,17 +10180,17 @@ - Regression: Blank screen in Jitsi video calls ([#23322](https://github.com/RocketChat/Rocket.Chat/pull/23322)) - - Fix Jitsi calls being disposed even when "Open in new window" setting is disabled; + - Fix Jitsi calls being disposed even when "Open in new window" setting is disabled; - Fix misspelling on `CallJitsWithData.js` file name. - Regression: Create new loggers based on server log level ([#23297](https://github.com/RocketChat/Rocket.Chat/pull/23297)) -- Regression: Fix app storage migration ([#23286](https://github.com/RocketChat/Rocket.Chat/pull/23286)) - - The previous version of this migration didn't take into consideration apps that were installed prior to [Rocket.Chat@3.8.0](https://github.com/RocketChat/Rocket.Chat/releases/tag/3.8.0), which [removed the typescript compiler from the server](https://github.com/RocketChat/Rocket.Chat/pull/18687) and into the CLI. As a result, the zip files inside each installed app's document in the database had typescript files in them instead of the now required javascript files. - - As the new strategy of source code storage for apps changes the way the app is loaded, those zip files containing the source code are read everytime the app is started (or [in this particular case, updated](https://github.com/RocketChat/Rocket.Chat/pull/23286/files#diff-caf9f7a22478639e58d6514be039140a42ce1ab2d999c3efe5678c38ee36d0ccR43)), and as the zips' contents were wrong, the operation was failing. +- Regression: Fix app storage migration ([#23286](https://github.com/RocketChat/Rocket.Chat/pull/23286)) + The previous version of this migration didn't take into consideration apps that were installed prior to [Rocket.Chat@3.8.0](https://github.com/RocketChat/Rocket.Chat/releases/tag/3.8.0), which [removed the typescript compiler from the server](https://github.com/RocketChat/Rocket.Chat/pull/18687) and into the CLI. As a result, the zip files inside each installed app's document in the database had typescript files in them instead of the now required javascript files. + + As the new strategy of source code storage for apps changes the way the app is loaded, those zip files containing the source code are read everytime the app is started (or [in this particular case, updated](https://github.com/RocketChat/Rocket.Chat/pull/23286/files#diff-caf9f7a22478639e58d6514be039140a42ce1ab2d999c3efe5678c38ee36d0ccR43)), and as the zips' contents were wrong, the operation was failing. + The fix extract the data from old apps and creates new zip files with the compiled `js` already present. - Regression: Fix Bugsnag not started error ([#23308](https://github.com/RocketChat/Rocket.Chat/pull/23308)) @@ -10445,10 +10397,8 @@ - **ENTERPRISE:** Maximum waiting time for chats in Omnichannel queue ([#22955](https://github.com/RocketChat/Rocket.Chat/pull/22955)) - - Add new settings to support closing chats that have been too long on waiting queue - - - Moved old settings to new "Queue Management" section - + - Add new settings to support closing chats that have been too long on waiting queue + - Moved old settings to new "Queue Management" section - Fix issue when closing a livechat room that caused client to not to know if room was open or not - Banner for the updates regarding authentication services ([#23055](https://github.com/RocketChat/Rocket.Chat/pull/23055) by [@g-thome](https://github.com/g-thome)) @@ -10463,10 +10413,10 @@ - Separate RegEx Settings for Channels and Usernames validation ([#21937](https://github.com/RocketChat/Rocket.Chat/pull/21937) by [@aditya-mitra](https://github.com/aditya-mitra)) - Now, there are 2 separate settings for validating names - One for **channels** and another for **usernames**. - - This change also removes the old `UTF8_Names_Validation` setting and adds 2 new settings `UTF8_User_Names_Validation` and `UTF8_Channel_Names_Validation`. - + Now, there are 2 separate settings for validating names - One for **channels** and another for **usernames**. + + This change also removes the old `UTF8_Names_Validation` setting and adds 2 new settings `UTF8_User_Names_Validation` and `UTF8_Channel_Names_Validation`. + https://user-images.githubusercontent.com/55396651/116969904-af5bb800-acd4-11eb-9fc4-dacac60cb08f.mp4 ### 🚀 Improvements @@ -10482,13 +10432,13 @@ - Rewrite File Upload Modal ([#22750](https://github.com/RocketChat/Rocket.Chat/pull/22750)) - Image preview: -  - - Video preview: -  - - Files larger than 10mb: + Image preview: +  + + Video preview: +  + + Files larger than 10mb:  - Types from currentChatsPage.tsx ([#22967](https://github.com/RocketChat/Rocket.Chat/pull/22967)) @@ -10504,14 +10454,14 @@ - "Users By Time of the Day" chart displays incorrect data for Local Timezone ([#22836](https://github.com/RocketChat/Rocket.Chat/pull/22836)) - - Add local timezone conversion to the "Users By Time of the Day" chart in the Engagement Dashboard; + - Add local timezone conversion to the "Users By Time of the Day" chart in the Engagement Dashboard; - Simplify date creations by using `endOf` and `startOf` methods. - Atlassian Crowd connection not working ([#22996](https://github.com/RocketChat/Rocket.Chat/pull/22996) by [@piotrkochan](https://github.com/piotrkochan)) - Audio recording doesn't stop in direct messages on channel switch ([#22880](https://github.com/RocketChat/Rocket.Chat/pull/22880)) - - Cancel audio recordings on message bar destroy event. + - Cancel audio recordings on message bar destroy event.  - Bad words falling if message is empty ([#22930](https://github.com/RocketChat/Rocket.Chat/pull/22930)) @@ -10536,23 +10486,21 @@ - Return transcript/dashboards based on timezone settings ([#22850](https://github.com/RocketChat/Rocket.Chat/pull/22850)) - - Added new setting to manage timezones - - - Applied new setting to omnichannel dashboards (realtime, analytics) [NOTE: Other dashboards aren't using this setting actually) - + - Added new setting to manage timezones + - Applied new setting to omnichannel dashboards (realtime, analytics) [NOTE: Other dashboards aren't using this setting actually) - Change getAnalyticsBetweenDate query to filter out system messages instead of substracting them - Tab margin style ([#22851](https://github.com/RocketChat/Rocket.Chat/pull/22851)) - ### before -  - - ### after + ### before +  + + ### after  - Threads and discussions searches don't display proper results ([#22914](https://github.com/RocketChat/Rocket.Chat/pull/22914)) - - _Fix_ issue in discussions search (which wasn't working after a search with no results was made); + - _Fix_ issue in discussions search (which wasn't working after a search with no results was made); - _Improve_ discussions and threads searches: both searches (`chat.getDiscussions` and `chat.getThreadsList`) are now case insensitive (do NOT differ capital from lower letters) and match incomplete words or terms. - Threads List being requested more than expected ([#22879](https://github.com/RocketChat/Rocket.Chat/pull/22879)) @@ -10587,8 +10535,8 @@ - Chore: Script to start Rocket.Chat in HA mode during development ([#22398](https://github.com/RocketChat/Rocket.Chat/pull/22398)) - Sometimes we need to start Rocket.Chat in High-Availability mode (cluster) during development to test how a feature behaves or hunt down a bug. Currently, this involves a lot of commands with details that might be lost if you haven't done it in a while. - + Sometimes we need to start Rocket.Chat in High-Availability mode (cluster) during development to test how a feature behaves or hunt down a bug. Currently, this involves a lot of commands with details that might be lost if you haven't done it in a while. + This PR intends to provide a really simple way for us to start many instances of Rocket.Chat connected in a cluster. - Chore: Update Livechat widget to 1.9.4 ([#22990](https://github.com/RocketChat/Rocket.Chat/pull/22990)) @@ -10605,13 +10553,13 @@ - Regression: File upload name suggestion ([#22953](https://github.com/RocketChat/Rocket.Chat/pull/22953)) - Before: -  -  - - - After: -  + Before: +  +  + + + After: +   - Regression: Fix creation of self-DMs ([#23015](https://github.com/RocketChat/Rocket.Chat/pull/23015)) @@ -10679,8 +10627,7 @@ - Fix Auto Selection algorithm on community edition ([#22991](https://github.com/RocketChat/Rocket.Chat/pull/22991)) - - When using the autoselection algo on community editions, all agents were marked as unavailable due to an unapplied filter - + - When using the autoselection algo on community editions, all agents were marked as unavailable due to an unapplied filter - Fixed an issue when both user & system setting to manange EE max number of chats allowed were set to 0 <details> @@ -10720,7 +10667,7 @@ - Apps-Engine's scheduler failing to update run tasks ([#22882](https://github.com/RocketChat/Rocket.Chat/pull/22882)) - [Agenda](https://github.com/agenda/agenda), the library that manages scheduling, depended on setting a job property named `nextRunAt` as `undefined` to signal whether it should be run on schedule or not. [Rocket.Chat's current Mongo driver](https://github.com/RocketChat/Rocket.Chat/pull/22399) ignores `undefined` values when updating documents and this was causing jobs to never stop running as Agenda couldn't clear that property (set them as `undefined`). + [Agenda](https://github.com/agenda/agenda), the library that manages scheduling, depended on setting a job property named `nextRunAt` as `undefined` to signal whether it should be run on schedule or not. [Rocket.Chat's current Mongo driver](https://github.com/RocketChat/Rocket.Chat/pull/22399) ignores `undefined` values when updating documents and this was causing jobs to never stop running as Agenda couldn't clear that property (set them as `undefined`). This updates Rocket.Chat's dependency on Agenda.js to point to [a fork that fixes the problem](https://github.com/RocketChat/agenda/releases/tag/3.1.2). - Close omnichannel conversations when agent is deactivated ([#22917](https://github.com/RocketChat/Rocket.Chat/pull/22917)) @@ -10774,7 +10721,7 @@ - Monitoring Track messages' round trip time ([#22676](https://github.com/RocketChat/Rocket.Chat/pull/22676)) - Track messages' roundtrip time from backend saves time to the time when received back from the oplog allowing track of oplog slowness. + Track messages' roundtrip time from backend saves time to the time when received back from the oplog allowing track of oplog slowness. Prometheus metric: `rocketchat_messages_roundtrip_time` - REST endpoint to remove User from Role ([#20485](https://github.com/RocketChat/Rocket.Chat/pull/20485) by [@Cosnavel](https://github.com/Cosnavel) & [@lucassartor](https://github.com/lucassartor) & [@ostjen](https://github.com/ostjen)) @@ -10786,22 +10733,19 @@ - Change message deletion confirmation modal to toast ([#22544](https://github.com/RocketChat/Rocket.Chat/pull/22544)) - Changed a timed modal for a toast message + Changed a timed modal for a toast message  - Configuration for indices in Apps-Engine models ([#22705](https://github.com/RocketChat/Rocket.Chat/pull/22705)) - * Add `appId` field to the data saved by the Scheduler - - * Add `appId` index to `rocketchat_apps_persistence` model - - * Skip "trash collection" when deleting records from `rocketchat_apps_persistence` - - * Add a new setting to control for how long we should keep logs from the apps - -  - - + * Add `appId` field to the data saved by the Scheduler + * Add `appId` index to `rocketchat_apps_persistence` model + * Skip "trash collection" when deleting records from `rocketchat_apps_persistence` + * Add a new setting to control for how long we should keep logs from the apps + +  + +  - Make `shortcut` field of canned responses unique ([#22700](https://github.com/RocketChat/Rocket.Chat/pull/22700)) @@ -10824,38 +10768,37 @@ - Replace remaing discussion creation modals with React modal. ([#22448](https://github.com/RocketChat/Rocket.Chat/pull/22448)) - ### before -  - - ### after + ### before +  + + ### after  - Return open room if available for visitors ([#22742](https://github.com/RocketChat/Rocket.Chat/pull/22742)) - Rewrite Enter Encryption Password Modal ([#22456](https://github.com/RocketChat/Rocket.Chat/pull/22456)) - ### before -  - - ### after -  - - ### Aditional Improves: - + ### before +  + + ### after +  + + ### Aditional Improves: - Added a visual validation in the password field - Rewrite OTR modals ([#22583](https://github.com/RocketChat/Rocket.Chat/pull/22583)) -  -  +  +   - Rewrite Save Encryption Password Modal ([#22447](https://github.com/RocketChat/Rocket.Chat/pull/22447)) - ### before -  - - ### after + ### before +  + + ### after  - Rewrite sidebar footer as React Component ([#22687](https://github.com/RocketChat/Rocket.Chat/pull/22687)) @@ -10870,12 +10813,12 @@ - Wrong error message when trying to create a blocked username ([#22452](https://github.com/RocketChat/Rocket.Chat/pull/22452) by [@lucassartor](https://github.com/lucassartor)) - When trying to create a user with a blocked username, the UI was showing generic error message that it wasn't very detailed. - - Old error message: -  - - New error message: + When trying to create a user with a blocked username, the UI was showing generic error message that it wasn't very detailed. + + Old error message: +  + + New error message:  ### 🛠Bug fixes @@ -10883,19 +10826,19 @@ - **ENTERPRISE:** Engagement Dashboard displaying incorrect data about active users ([#22381](https://github.com/RocketChat/Rocket.Chat/pull/22381)) - - Fix sessions' and users' grouping in the Engagement Dashboard API endpoints; - - Fix the data displayed in the charts from the "Active users", "Users by time of day" and "When is the chat busier?" sections of the Engagement Dashboard; + - Fix sessions' and users' grouping in the Engagement Dashboard API endpoints; + - Fix the data displayed in the charts from the "Active users", "Users by time of day" and "When is the chat busier?" sections of the Engagement Dashboard; - Replace label used to describe the amount of Active Users in the License section of the Info page. - **ENTERPRISE:** Make AutoSelect algo take current agent load in consideration ([#22611](https://github.com/RocketChat/Rocket.Chat/pull/22611)) - **ENTERPRISE:** Race condition on Omnichannel visitor abandoned callback ([#22413](https://github.com/RocketChat/Rocket.Chat/pull/22413)) - As you can see [here](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/lib/Helper.js#L127) the `predictedVisitorAbandonment` flag is not set if the room object doesn't have `v.lastMessageTs` property. So we need to always make sure the `v.lastMessageTs` is set before this method is called. - - Currently the `v.lastMessageTs` is being set in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/app/livechat/server/hooks/saveLastVisitorMessageTs.js#L4) (lets call this **hook-1**) hook which has `HIGH` priority - and the `predictedVisitorAbandonment` check is inturn performed in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js#L5) (let call this **hook-2**) hook which is also `HIGH` priority. - + As you can see [here](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/lib/Helper.js#L127) the `predictedVisitorAbandonment` flag is not set if the room object doesn't have `v.lastMessageTs` property. So we need to always make sure the `v.lastMessageTs` is set before this method is called. + + Currently the `v.lastMessageTs` is being set in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/app/livechat/server/hooks/saveLastVisitorMessageTs.js#L4) (lets call this **hook-1**) hook which has `HIGH` priority + and the `predictedVisitorAbandonment` check is inturn performed in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js#L5) (let call this **hook-2**) hook which is also `HIGH` priority. + So ideally we'd except the **hook-1** to be called b4 **hook-2**, however currently since both of them are at same priority, there is no way to control which one is executed first. Hence in this PR, I'm making the priority of **hook-2** as `MEDIUM` to keeping the priority of **hook-1** the same as b4, i.e. `HIGH`. This should make sure that the **hook-1** is always executed b4 **hook-2** - Admin page crashing when commit hash is null ([#22057](https://github.com/RocketChat/Rocket.Chat/pull/22057) by [@cprice-kgi](https://github.com/cprice-kgi)) @@ -10904,41 +10847,39 @@ - Blank screen in message auditing DM tab ([#22763](https://github.com/RocketChat/Rocket.Chat/pull/22763) by [@ostjen](https://github.com/ostjen)) - The DM tab in message auditing was displaying a blank screen, instead of the actual tab. - + The DM tab in message auditing was displaying a blank screen, instead of the actual tab. +  - Bugs in AutoCompleteDepartment ([#22414](https://github.com/RocketChat/Rocket.Chat/pull/22414)) - Call button is still displayed when the user doesn't have permission to use it ([#22170](https://github.com/RocketChat/Rocket.Chat/pull/22170)) - - Hide 'Call' buttons from the tab bar for muted users; - + - Hide 'Call' buttons from the tab bar for muted users; - Display an error when a muted user attempts to enter a call using the 'Click to Join!' button. - Can't see full user profile on team's room ([#22355](https://github.com/RocketChat/Rocket.Chat/pull/22355)) - ### before -  - - ### after -  - - ### aditional fix :rocket: - + ### before +  + + ### after +  + + ### aditional fix :rocket: - unnecessary `TeamsMembers` component removed - Cannot create a discussion from top left sidebar as a user ([#22618](https://github.com/RocketChat/Rocket.Chat/pull/22618) by [@lucassartor](https://github.com/lucassartor)) - When trying to create a discussion using the top left sidebar modal with an role that don't have the `view-other-user-channels ` permission, an empty list would be shown, which is a wrong behavior. - Also, when being able to use this modal, discussions were listed as options, which is also a wrong behavior as there can't be nested discussions. - - This PR looks to fix both these issues. - - **Old behavior:** -  - - **New behavior:** + When trying to create a discussion using the top left sidebar modal with an role that don't have the `view-other-user-channels ` permission, an empty list would be shown, which is a wrong behavior. + Also, when being able to use this modal, discussions were listed as options, which is also a wrong behavior as there can't be nested discussions. + + This PR looks to fix both these issues. + + **Old behavior:** +  + + **New behavior:**  - Channel is automatically getting added to the first option in move to team feature ([#22670](https://github.com/RocketChat/Rocket.Chat/pull/22670) by [@ostjen](https://github.com/ostjen)) @@ -10953,12 +10894,12 @@ - Create discussion modal - cancel button and invite users alignment ([#22718](https://github.com/RocketChat/Rocket.Chat/pull/22718) by [@ostjen](https://github.com/ostjen)) - Changes in "open discussion" modal - - > Added cancel button - > Fixed alignment in invite user - - + Changes in "open discussion" modal + + > Added cancel button + > Fixed alignment in invite user + +  - crush in the getChannelHistory method ([#22667](https://github.com/RocketChat/Rocket.Chat/pull/22667) by [@MaestroArt](https://github.com/MaestroArt)) @@ -10993,29 +10934,27 @@ - Quote message not working for Livechat visitors ([#22586](https://github.com/RocketChat/Rocket.Chat/pull/22586)) - ### Before: -  - ### After: + ### Before: +  + ### After:  - Redirect to login after delete own account ([#22499](https://github.com/RocketChat/Rocket.Chat/pull/22499)) - Redirect the user to login after delete own account - - ### Aditional fixes: - - - Visual issue in password input on Delete Own Account Modal - - ### before -  - - ### after + Redirect the user to login after delete own account + + ### Aditional fixes: + - Visual issue in password input on Delete Own Account Modal + + ### before +  + + ### after  - Remove stack traces from Meteor errors when debug setting is disabled ([#22699](https://github.com/RocketChat/Rocket.Chat/pull/22699)) - - Fix 'not iterable' errors in the `normalizeMessage` function; - + - Fix 'not iterable' errors in the `normalizeMessage` function; - Remove stack traces from errors thrown by the `jitsi:updateTimeout` (and other `Meteor.Error`s) method. - Rewrite CurrentChats to TS ([#22424](https://github.com/RocketChat/Rocket.Chat/pull/22424)) @@ -11104,16 +11043,15 @@ - Regression: Data in the "Active Users" section is delayed in 1 day ([#22794](https://github.com/RocketChat/Rocket.Chat/pull/22794)) - - Fix 1 day delay in the Engagement Dashboard's "Active Users" section; - - - Downgrade `@nivo/line` version. - **Expected behavior:** + - Fix 1 day delay in the Engagement Dashboard's "Active Users" section; + - Downgrade `@nivo/line` version. + **Expected behavior:**  - Regression: Data in the "New Users" section is delayed in 1 day ([#22751](https://github.com/RocketChat/Rocket.Chat/pull/22751)) - - Update nivo version (which was causing errors in the bar chart); - - Fix 1 day delay in '7 days' and '30 days' periods; + - Update nivo version (which was causing errors in the bar chart); + - Fix 1 day delay in '7 days' and '30 days' periods; - Update tooltip theme. - Regression: Federation warnings on ci ([#22765](https://github.com/RocketChat/Rocket.Chat/pull/22765) by [@g-thome](https://github.com/g-thome)) @@ -11138,9 +11076,9 @@ - Regression: Fix tooltip style in the "Busiest Chat Times" chart ([#22813](https://github.com/RocketChat/Rocket.Chat/pull/22813)) - - Fix tooltip in the Engagement Dashboard's "Busiest Chat Times" chart (Hours). - - **Expected behavior:** + - Fix tooltip in the Engagement Dashboard's "Busiest Chat Times" chart (Hours). + + **Expected behavior:**  - Regression: Fix users not being able to see the scope of the canned m… ([#22760](https://github.com/RocketChat/Rocket.Chat/pull/22760)) @@ -11157,10 +11095,10 @@ - Regression: Prevent custom status from being visible in sequential messages ([#22733](https://github.com/RocketChat/Rocket.Chat/pull/22733)) - ### before -  - - ### after + ### before +  + + ### after  - Regression: Properly force newline in attachment fields ([#22727](https://github.com/RocketChat/Rocket.Chat/pull/22727)) @@ -11341,32 +11279,30 @@ - Add `teams.convertToChannel` endpoint ([#22188](https://github.com/RocketChat/Rocket.Chat/pull/22188)) - - Add new `teams.converToChannel` endpoint; - - - Update `ConvertToTeam` modal text (since this action can now be reversed); - + - Add new `teams.converToChannel` endpoint; + - Update `ConvertToTeam` modal text (since this action can now be reversed); - Remove corresponding team memberships when a team is deleted or converted to a channel; - Add setting to configure default role for user on manual registration ([#20650](https://github.com/RocketChat/Rocket.Chat/pull/20650) by [@lucassartor](https://github.com/lucassartor)) - Add an `admin` setting to determine the initial `role` for new users who registered manually (through the register form and via API, not using an authentication service), normally all new users are assigned to the `user` role. - - The setting can be found in `Admin`->`Accounts`->`Registration`. - -  - The setting initial value is false, so the default behaviour stays the same while creating a new server or upgrading one. - - https://user-images.githubusercontent.com/49413772/107253220-ddeb2f00-6a14-11eb-85b4-f770dbbe4970.mp4 - + Add an `admin` setting to determine the initial `role` for new users who registered manually (through the register form and via API, not using an authentication service), normally all new users are assigned to the `user` role. + + The setting can be found in `Admin`->`Accounts`->`Registration`. + +  + The setting initial value is false, so the default behaviour stays the same while creating a new server or upgrading one. + + https://user-images.githubusercontent.com/49413772/107253220-ddeb2f00-6a14-11eb-85b4-f770dbbe4970.mp4 + Video showing an example of the setting being used and creating an new user with the default roles via API. - Content-Security-Policy for inline scripts ([#20724](https://github.com/RocketChat/Rocket.Chat/pull/20724)) - Security policies were applied for inline scripts cases. Due to the libraries and components we use it is not possible to disable inline styles and images as they would break Oembeds and other libraries. - - - basically the inline scripts were moved to a js file - + Security policies were applied for inline scripts cases. Due to the libraries and components we use it is not possible to disable inline styles and images as they would break Oembeds and other libraries. + + + basically the inline scripts were moved to a js file + and besides that some suggars syntax like `addScript` and `addStyle` were added, this way the application already takes care of inserting the elements and providing the content automatically. - Open modals in side effects outside React ([#22247](https://github.com/RocketChat/Rocket.Chat/pull/22247)) @@ -11382,17 +11318,15 @@ - Add BBB and Jitsi to Team ([#22312](https://github.com/RocketChat/Rocket.Chat/pull/22312)) - Added 2 new settings: - - - `Admin > Video Conference > Big Blue Button > Enable for teams` - + Added 2 new settings: + - `Admin > Video Conference > Big Blue Button > Enable for teams` - `Admin > Video Conference > Jitsi > Enable in teams` - Add debouncing to units selects filters ([#22097](https://github.com/RocketChat/Rocket.Chat/pull/22097)) - Add modal to close chats when tags/comments are not required ([#22245](https://github.com/RocketChat/Rocket.Chat/pull/22245) by [@rafaelblink](https://github.com/rafaelblink)) - When neither tags or comments are required to close a livechat, show this modal instead: + When neither tags or comments are required to close a livechat, show this modal instead:  - Fallback messages on contextual bar ([#22376](https://github.com/RocketChat/Rocket.Chat/pull/22376)) @@ -11415,10 +11349,10 @@ - Remove differentiation between public x private channels in sidebar ([#22160](https://github.com/RocketChat/Rocket.Chat/pull/22160)) - ### before -  - - ### after + ### before +  + + ### after  - Rewrite create direct modal ([#22209](https://github.com/RocketChat/Rocket.Chat/pull/22209)) @@ -11427,8 +11361,8 @@ - Rewrite Create Discussion Modal (only through sidebar) ([#22224](https://github.com/RocketChat/Rocket.Chat/pull/22224)) - This is only available by creating a new discussion when clicking on the sidebar button. Other places will be implemented afterwards. - + This is only available by creating a new discussion when clicking on the sidebar button. Other places will be implemented afterwards. +  - Send only relevant data via WebSocket ([#22258](https://github.com/RocketChat/Rocket.Chat/pull/22258)) @@ -11442,12 +11376,12 @@ - **EE:** Canned responses can't be deleted ([#22095](https://github.com/RocketChat/Rocket.Chat/pull/22095) by [@rafaelblink](https://github.com/rafaelblink)) - Deletion button has been removed from the edition option. - - ## Before -  - - ### After + Deletion button has been removed from the edition option. + + ## Before +  + + ### After  - **ENTERPRISE:** Omnichannel enterprise permissions being added back to its default roles ([#22322](https://github.com/RocketChat/Rocket.Chat/pull/22322)) @@ -11456,19 +11390,19 @@ - **ENTERPRISE:** Prevent Visitor Abandonment after forwarding chat ([#22243](https://github.com/RocketChat/Rocket.Chat/pull/22243)) - Currently the Visitor Abandonment timer isn't affected when the chat is forwarded. However this is affecting the UX in certain situations like eg: A bot forwarding a chat to an human agent -  - + Currently the Visitor Abandonment timer isn't affected when the chat is forwarded. However this is affecting the UX in certain situations like eg: A bot forwarding a chat to an human agent +  + To solve this issue, we'll now be stoping the Visitor Abandonment timer once a chat is forwarded. - **IMPROVE:** Prevent creation of duplicated roles and new `roles.update` endpoint ([#22279](https://github.com/RocketChat/Rocket.Chat/pull/22279) by [@lucassartor](https://github.com/lucassartor)) - Currently, the action of updating a role is broken: because roles have their `_id` = `name`, when updating a role there's no way to validate if the user is trying to update or create a new role with a name that already exists - which causes wrong behaviors, such as roles with the same name and not being able to update them. - - To proper fix this, this PR looks to change the creation of roles. Now, roles have a unique `_id` value and there's a endpoint to update roles: `/api/v1/roles.update`. - - Doing so, it's possible to validate on both endpoints (`roles.create` and `roles.update`) to not allow roles with duplicated names. - + Currently, the action of updating a role is broken: because roles have their `_id` = `name`, when updating a role there's no way to validate if the user is trying to update or create a new role with a name that already exists - which causes wrong behaviors, such as roles with the same name and not being able to update them. + + To proper fix this, this PR looks to change the creation of roles. Now, roles have a unique `_id` value and there's a endpoint to update roles: `/api/v1/roles.update`. + + Doing so, it's possible to validate on both endpoints (`roles.create` and `roles.update`) to not allow roles with duplicated names. + **OBS:** The unique id changes only reflect new roles, the standard roles (such as admin and user) still have `_id` = `name`, but new roles now **can't** have the same name as them. - `channels.history`, `groups.history` and `im.history` REST endpoints not respecting hide system message config ([#22364](https://github.com/RocketChat/Rocket.Chat/pull/22364)) @@ -11485,10 +11419,10 @@ - Can't delete file from Room's file list ([#22191](https://github.com/RocketChat/Rocket.Chat/pull/22191)) - ### before -  - - ### after + ### before +  + + ### after  - Cancel button and success toast at Leave Team modal ([#22373](https://github.com/RocketChat/Rocket.Chat/pull/22373)) @@ -11499,10 +11433,10 @@ - Convert and Move team permission ([#22350](https://github.com/RocketChat/Rocket.Chat/pull/22350)) - ### before - https://user-images.githubusercontent.com/45966964/114909360-5c04f100-9e1d-11eb-9363-f308e5d0be68.mp4 - - ### after + ### before + https://user-images.githubusercontent.com/45966964/114909360-5c04f100-9e1d-11eb-9363-f308e5d0be68.mp4 + + ### after https://user-images.githubusercontent.com/45966964/114909388-61fad200-9e1d-11eb-9bbe-114b55954a9f.mp4 - CORS error while interacting with any action button on Livechat ([#22150](https://github.com/RocketChat/Rocket.Chat/pull/22150)) @@ -11521,50 +11455,50 @@ - Members tab visual issues ([#22138](https://github.com/RocketChat/Rocket.Chat/pull/22138)) - ## Before -  - - ## After + ## Before +  + + ## After  - Memory leak generated by Stream Cast usage ([#22329](https://github.com/RocketChat/Rocket.Chat/pull/22329)) - Stream Cast uses a different approach to broadcast data to the instances, it uses the DDP subscription method that requires a collection on the other side, if no collection exists with the given name `broadcast-stream` it caches in memory waiting for the collection to be set later. The cache is cleared only when a reconnection happens. - + Stream Cast uses a different approach to broadcast data to the instances, it uses the DDP subscription method that requires a collection on the other side, if no collection exists with the given name `broadcast-stream` it caches in memory waiting for the collection to be set later. The cache is cleared only when a reconnection happens. + This PR overrides the function that processes the data for that specific connection, preventing the cache and everything else to be processed since we already have our low-level listener to process the data. - Message box hiding on mobile view (Safari) ([#22212](https://github.com/RocketChat/Rocket.Chat/pull/22212)) - ### before -  - - ### after + ### before +  + + ### after  - Missing burger menu on direct messages ([#22211](https://github.com/RocketChat/Rocket.Chat/pull/22211)) - ### before -  - - ### after + ### before +  + + ### after  - Missing Throbber while thread list is loading ([#22316](https://github.com/RocketChat/Rocket.Chat/pull/22316)) - ### before - List was starting with no results even if there's results: - -  - - ### after + ### before + List was starting with no results even if there's results: + +  + + ### after  - Not possible to edit some messages inside threads ([#22325](https://github.com/RocketChat/Rocket.Chat/pull/22325)) - ### Before -  - - ### After + ### Before +  + + ### After  - Notifications not using user's name ([#22309](https://github.com/RocketChat/Rocket.Chat/pull/22309)) @@ -11591,10 +11525,10 @@ - Sidebar not closing when clicking on a channel ([#22271](https://github.com/RocketChat/Rocket.Chat/pull/22271)) - ### before -  - - ### after + ### before +  + + ### after  - Sound notification is not emitted when the Omnichannel chat comes from another department ([#22291](https://github.com/RocketChat/Rocket.Chat/pull/22291)) @@ -11605,9 +11539,9 @@ - Undefined error when forwarding chats to offline department ([#22154](https://github.com/RocketChat/Rocket.Chat/pull/22154) by [@rafaelblink](https://github.com/rafaelblink)) -  - - Omnichannel agents are facing the error shown above when forwarding chats to offline departments. +  + + Omnichannel agents are facing the error shown above when forwarding chats to offline departments. The error usually takes place when the routing system algorithm is **Manual Selection**. - Unread bar in channel flash quickly and then disappear ([#22275](https://github.com/RocketChat/Rocket.Chat/pull/22275)) @@ -11638,15 +11572,15 @@ - Chore: Change modals for remove user from team && leave team ([#22141](https://github.com/RocketChat/Rocket.Chat/pull/22141)) -  +   - Chore: Check PR Title on every submission ([#22140](https://github.com/RocketChat/Rocket.Chat/pull/22140)) - Chore: Enable push gateway only if the server is registered ([#22346](https://github.com/RocketChat/Rocket.Chat/pull/22346) by [@lucassartor](https://github.com/lucassartor)) - Currently, when creating an unregistered server, the default value of the push gateway setting is set to true and is disabled (it can't be changed unless the server is registered). This is a wrong behavior as an unregistered server **can't** use the push gateway. - + Currently, when creating an unregistered server, the default value of the push gateway setting is set to true and is disabled (it can't be changed unless the server is registered). This is a wrong behavior as an unregistered server **can't** use the push gateway. + This PR creates a validation to check if the server is registered when enabling the push gateway. That way, even if the push gateway setting is turned on, but the server is unregistered, the push gateway **won't** work - it will behave like it is off. - Chore: Enforce TypeScript on Storybook ([#22317](https://github.com/RocketChat/Rocket.Chat/pull/22317)) @@ -11663,7 +11597,7 @@ - Chore: Update delete team modal to new design ([#22127](https://github.com/RocketChat/Rocket.Chat/pull/22127)) - Now the modal has only 2 steps (steps 1 and 2 were merged) + Now the modal has only 2 steps (steps 1 and 2 were merged)  - Language update from LingoHub 🤖 on 2021-05-31Z ([#22196](https://github.com/RocketChat/Rocket.Chat/pull/22196)) @@ -11690,10 +11624,10 @@ - Regression: Missing flexDirection on select field ([#22300](https://github.com/RocketChat/Rocket.Chat/pull/22300)) - ### before -  - - ### after + ### before +  + + ### after  - Regression: RoomProvider using wrong types ([#22370](https://github.com/RocketChat/Rocket.Chat/pull/22370)) @@ -11834,50 +11768,50 @@ - **ENTERPRISE:** Introduce Load Rotation routing algorithm for Omnichannel ([#22090](https://github.com/RocketChat/Rocket.Chat/pull/22090) by [@rafaelblink](https://github.com/rafaelblink)) - This PR introduces a new Auto Chat Distribution (ACD) algorithm for Omnichannel: **Load Rotation**. - The algorithm distributes chats to agents one by one, which means that when a new chat arrives, the agent with the oldest routing assignment time will be selected to serve the chat, regardless of the number of chats in progress each agent has. - + This PR introduces a new Auto Chat Distribution (ACD) algorithm for Omnichannel: **Load Rotation**. + The algorithm distributes chats to agents one by one, which means that when a new chat arrives, the agent with the oldest routing assignment time will be selected to serve the chat, regardless of the number of chats in progress each agent has. +  - Back button for Omnichannel ([#21647](https://github.com/RocketChat/Rocket.Chat/pull/21647) by [@rafaelblink](https://github.com/rafaelblink)) - New Message Parser ([#21962](https://github.com/RocketChat/Rocket.Chat/pull/21962)) - The objective is to put an end to the confusion that we face having multiple parsers, and the problems that this brings, it is still experimental then users need to choose to use it. - - The benefits are multiple. no more unexpected cases or grammatical collisions (in addition to more flexible nested cases like bold within link labels). - Besides, we no longer render raw html, instead we use components, so the xss attacks are over (the easy ones at least). Without further discoveries and at the fronted, we only reder what is delivered thus improving our performance. + The objective is to put an end to the confusion that we face having multiple parsers, and the problems that this brings, it is still experimental then users need to choose to use it. + + The benefits are multiple. no more unexpected cases or grammatical collisions (in addition to more flexible nested cases like bold within link labels). + Besides, we no longer render raw html, instead we use components, so the xss attacks are over (the easy ones at least). Without further discoveries and at the fronted, we only reder what is delivered thus improving our performance. This can be used in multiple places, (message, alert, sidenav and in the entire mobile application.) - Option to notify failed login attempts to a channel ([#21968](https://github.com/RocketChat/Rocket.Chat/pull/21968)) - Option to prevent users from using Invisible status ([#20084](https://github.com/RocketChat/Rocket.Chat/pull/20084) by [@lucassartor](https://github.com/lucassartor)) - Add an `admin` option to allow/disallow the `Invisible` status option from all users. This option is available in the `Accounts` section. - -  - - If the option is turned off, the `users.setStatus` endpoint is also restricted from users trying to change their status to `Invisible`, throwing the following error: - ```json - { - "success": false, - "error": "Invisible status is disabled [error-not-allowed]", - "stack": "Error: Invisible status is disabled [error-not-allowed]\n at DDPCommon.MethodInvocation.<anonymous> (app/api/server/v1/users.js:425:13)\n at packages/dispatch_run-as-user.js:211:14\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object.Meteor.runAsUser (packages/dispatch_run-as-user.js:210:33)\n at Object.post (app/api/server/v1/users.js:415:10)\n at app/api/server/api.js:394:82\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object._internalRouteActionHandler [as action] (app/api/server/api.js:394:39)\n at Route.share.Route.Route._callEndpoint (packages/nimble_restivus/lib/route.coffee:150:32)\n at packages/nimble_restivus/lib/route.coffee:59:33\n at packages/simple_json-routes.js:98:9", - "errorType": "error-not-allowed", - "details": { - "method": "users.setStatus" - } - } + Add an `admin` option to allow/disallow the `Invisible` status option from all users. This option is available in the `Accounts` section. + +  + + If the option is turned off, the `users.setStatus` endpoint is also restricted from users trying to change their status to `Invisible`, throwing the following error: + ```json + { + "success": false, + "error": "Invisible status is disabled [error-not-allowed]", + "stack": "Error: Invisible status is disabled [error-not-allowed]\n at DDPCommon.MethodInvocation.<anonymous> (app/api/server/v1/users.js:425:13)\n at packages/dispatch_run-as-user.js:211:14\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object.Meteor.runAsUser (packages/dispatch_run-as-user.js:210:33)\n at Object.post (app/api/server/v1/users.js:415:10)\n at app/api/server/api.js:394:82\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object._internalRouteActionHandler [as action] (app/api/server/api.js:394:39)\n at Route.share.Route.Route._callEndpoint (packages/nimble_restivus/lib/route.coffee:150:32)\n at packages/nimble_restivus/lib/route.coffee:59:33\n at packages/simple_json-routes.js:98:9", + "errorType": "error-not-allowed", + "details": { + "method": "users.setStatus" + } + } ``` - Paginated and Filtered selects on new/edit unit ([#22052](https://github.com/RocketChat/Rocket.Chat/pull/22052) by [@rafaelblink](https://github.com/rafaelblink)) - REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 - - Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well - -  - + REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 + + Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well + +  + This Affects the monitors and departments inputs - Remove exif metadata from uploaded files ([#22044](https://github.com/RocketChat/Rocket.Chat/pull/22044)) @@ -11905,17 +11839,13 @@ - Inconsistent and misleading 2FA settings ([#22042](https://github.com/RocketChat/Rocket.Chat/pull/22042) by [@lucassartor](https://github.com/lucassartor)) - Currently, there are some inconsistencies and incorrect behaviors on the 2FA settings, such as: - - - - When disabling the TOTP 2FA, all 2FA are disabled; - - - There are no option to disable only the TOTP 2FA; - - - If 2FA are disabled, the other settings aren't blocked (the e-mail 2FA setting, for example); - - - It lacks some labels to warn the user of some specific 2FA options. - + Currently, there are some inconsistencies and incorrect behaviors on the 2FA settings, such as: + + - When disabling the TOTP 2FA, all 2FA are disabled; + - There are no option to disable only the TOTP 2FA; + - If 2FA are disabled, the other settings aren't blocked (the e-mail 2FA setting, for example); + - It lacks some labels to warn the user of some specific 2FA options. + This PR looks to fix those issues. - LDAP port setting input type to allow only numbers ([#21912](https://github.com/RocketChat/Rocket.Chat/pull/21912) by [@Deepak-learner](https://github.com/Deepak-learner)) @@ -11937,16 +11867,16 @@ - **APPS:** Scheduler duplicating recurrent tasks after server restart ([#21866](https://github.com/RocketChat/Rocket.Chat/pull/21866)) - Reintroduces the old method for creating recurring tasks in the apps' scheduler bridge to ensure tasks won't be duplicated. - - By introducing the [`skipImmediate` property option](https://github.com/RocketChat/Rocket.Chat/pull/21353) at the [`scheduleRecurring`](https://github.com/RocketChat/Rocket.Chat/blob/f8171f464ed8a7487795651767695fb33a1c709e/app/apps/server/bridges/scheduler.js#L119) method, the `every` method from _agenda.js_, which ensured no duplicates were created, was removed in favor of a more manual procedure. The new procedure was not taking into account the management of duplicates and as a result multiple copies of the same task could be created and they would get executed at the same time. - + Reintroduces the old method for creating recurring tasks in the apps' scheduler bridge to ensure tasks won't be duplicated. + + By introducing the [`skipImmediate` property option](https://github.com/RocketChat/Rocket.Chat/pull/21353) at the [`scheduleRecurring`](https://github.com/RocketChat/Rocket.Chat/blob/f8171f464ed8a7487795651767695fb33a1c709e/app/apps/server/bridges/scheduler.js#L119) method, the `every` method from _agenda.js_, which ensured no duplicates were created, was removed in favor of a more manual procedure. The new procedure was not taking into account the management of duplicates and as a result multiple copies of the same task could be created and they would get executed at the same time. + In the case of server restarts, every time this event happened and the app had the `startupSetting` configured to use _recurring tasks_, they would get recreated the same number of times. In the case of a server that restarts frequently (_n_ times), there would be the same (_n_) number of tasks duplicated (and running) in the system. - **ENTERPRISE:** Omnichannel Monitors can't forward chats to departments that they are not supervising ([#22128](https://github.com/RocketChat/Rocket.Chat/pull/22128)) - Currently, Omnichannel Monitors just can't forward chats to a department that is part of a `Business Unit` they're not supervising. This issue is causing critical problems on customer operations since this behaviour is not by design. - The reason this issue is taking place is that, by design, Monitors just have access to departments related to the `Business Units` they're monitoring, but this restriction is designed only for Omnichannel management areas, which means in case the monitor is, also, an agent, they're supposed to be able to forward a chat to any available departments regardless the `Business Units` it's associated with. + Currently, Omnichannel Monitors just can't forward chats to a department that is part of a `Business Unit` they're not supervising. This issue is causing critical problems on customer operations since this behaviour is not by design. + The reason this issue is taking place is that, by design, Monitors just have access to departments related to the `Business Units` they're monitoring, but this restriction is designed only for Omnichannel management areas, which means in case the monitor is, also, an agent, they're supposed to be able to forward a chat to any available departments regardless the `Business Units` it's associated with. So, initially, the restriction was implemented on the `Department Model` and, now, we're implementing the logic properly and introducing a new parameter to department endpoints, so the client will define which type of departments it needs. - **ENTERPRISE:** Omnichannel Monitors can't forward chats to departments that they are not supervising ([#22142](https://github.com/RocketChat/Rocket.Chat/pull/22142)) @@ -11985,18 +11915,18 @@ - Correcting a the wrong Archived label in edit room ([#21717](https://github.com/RocketChat/Rocket.Chat/pull/21717) by [@Jeanstaquet](https://github.com/Jeanstaquet)) -  - +  + A label exists for Archived, and it has not been used. So I replaced it with the existing one. the label 'Archived' does not exist. - Custom OAuth not being completely deleted ([#21637](https://github.com/RocketChat/Rocket.Chat/pull/21637) by [@siva2204](https://github.com/siva2204)) - Directory Table's Sort Function ([#21921](https://github.com/RocketChat/Rocket.Chat/pull/21921)) - ### TableRow Margin Issue: -  - - ### Table Sort Action Issue: + ### TableRow Margin Issue: +  + + ### Table Sort Action Issue:  - Discussion names showing a random value ([#22172](https://github.com/RocketChat/Rocket.Chat/pull/22172)) @@ -12007,54 +11937,54 @@ - Emails being sent with HTML entities getting escaped multiple times ([#21994](https://github.com/RocketChat/Rocket.Chat/pull/21994) by [@bhavayAnand9](https://github.com/bhavayAnand9)) - fixes an issue where if password contains special HTML character like &, in the email it would end up something like `&amp;` - - - password was going through multiple escapeHTML function calls - `secure&123 => secure&123 => secure&amp;123 + fixes an issue where if password contains special HTML character like &, in the email it would end up something like `&amp;` + + + password was going through multiple escapeHTML function calls + `secure&123 => secure&123 => secure&amp;123 ` - Error when you look at the members list of a room in which you are not a member ([#21952](https://github.com/RocketChat/Rocket.Chat/pull/21952) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Before, when you look at the members of a room in which you are not a member the app crashed, i corrected this problem. - Indeed, there was a check on each currentSubscription.<somehting> to see if it was not undefined except on currentSubscription.blocker - + Before, when you look at the members of a room in which you are not a member the app crashed, i corrected this problem. + Indeed, there was a check on each currentSubscription.<somehting> to see if it was not undefined except on currentSubscription.blocker + https://user-images.githubusercontent.com/45966964/117087470-d3101400-ad4f-11eb-8f44-0ebca830a4d8.mp4 - errors when viewing a room that you're not subscribed to ([#21984](https://github.com/RocketChat/Rocket.Chat/pull/21984)) - Files list will not show deleted files. ([#21732](https://github.com/RocketChat/Rocket.Chat/pull/21732) by [@Darshilp326](https://github.com/Darshilp326)) - When you delete files from the header option, deleted files will not be shown. - + When you delete files from the header option, deleted files will not be shown. + https://user-images.githubusercontent.com/55157259/115730786-38552400-a3a4-11eb-9684-7f510920db66.mp4 - Fixed the fact that when a team was deleted, not all channels were unlinked from the team ([#21942](https://github.com/RocketChat/Rocket.Chat/pull/21942) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Fixed the fact that when a team was deleted, not all channels were unlinked from the team. Only the first room of the rooms list was unlinked. - - After the fix, there is nos more errors: - - + Fixed the fact that when a team was deleted, not all channels were unlinked from the team. Only the first room of the rooms list was unlinked. + + After the fix, there is nos more errors: + + https://user-images.githubusercontent.com/45966964/117055182-2a47c180-ad1b-11eb-806f-07fb3fa7ec12.mp4 - Fixing Jitsi call ended Issue. ([#21808](https://github.com/RocketChat/Rocket.Chat/pull/21808)) - The new rewrite in react of contextual call component broke the Jitsi "click to join" messages. The issue being after 10 seconds of initiating the call, the message "click to join" always returned "Call Ended" even if the call was still going on. - This was due to the fact that after closing the contextual bar, the react component gets unmounted and we are not able to keep track of ongoing call and increase jitsi room timeout. - - This PR solves this issue by using the setInterval methods on component will unmount. When the call component unmounts, we keep on checking the state of jitsi call and based on conditions increase the jitsi room timeout. After the call is ended all setInterval calls are closed. - + The new rewrite in react of contextual call component broke the Jitsi "click to join" messages. The issue being after 10 seconds of initiating the call, the message "click to join" always returned "Call Ended" even if the call was still going on. + This was due to the fact that after closing the contextual bar, the react component gets unmounted and we are not able to keep track of ongoing call and increase jitsi room timeout. + + This PR solves this issue by using the setInterval methods on component will unmount. When the call component unmounts, we keep on checking the state of jitsi call and based on conditions increase the jitsi room timeout. After the call is ended all setInterval calls are closed. + This PR also removes the implementation of HEARTBEAT events of JitsiBridge. This is because this is no longer needed and all logic is being taken care of by the unmount function. - Handle NPS errors instead of throwing them ([#21945](https://github.com/RocketChat/Rocket.Chat/pull/21945)) - Header Tag Visual Issues ([#21991](https://github.com/RocketChat/Rocket.Chat/pull/21991)) - ### Normal -  - - ### Hover + ### Normal +  + + ### Hover  - Horizontal scrollbar not showing on tables ([#21852](https://github.com/RocketChat/Rocket.Chat/pull/21852)) @@ -12063,17 +11993,17 @@ - iFrame size on embedded videos ([#21992](https://github.com/RocketChat/Rocket.Chat/pull/21992)) - ### Before -  - - ### After + ### Before +  + + ### After  - Incorrect error message when opening channel in anonymous read ([#22066](https://github.com/RocketChat/Rocket.Chat/pull/22066) by [@lucassartor](https://github.com/lucassartor)) - Every time you open a public channel with threads in it when using anonymous read an `Incorrect User` error will be thrown. - This is an incorrect behaviour as everything that is public should be valid for an anonymous user. - + Every time you open a public channel with threads in it when using anonymous read an `Incorrect User` error will be thrown. + This is an incorrect behaviour as everything that is public should be valid for an anonymous user. + Some files are adapted to that and have already removed this kind of incorrect error, but there are some that need some fix, this PR aims to do that. - Incorrect Team's Info spacing ([#22021](https://github.com/RocketChat/Rocket.Chat/pull/22021)) @@ -12086,21 +12016,19 @@ - Make the FR translation consistent with the 'room' translation + typos ([#21913](https://github.com/RocketChat/Rocket.Chat/pull/21913) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - In the FR translation files, there were two terms that were used to refer to **'room'**: - - - 'salon' (149 times used) - -  - - - - 'salle' (46 times used) - -  - - The problem is that both were used in the same context and sometimes even in the same option list. - However, since 'salon' is a better translation and was also in the majority, I used the translation 'salon' wherever 'salle' was marked. - - For example: + In the FR translation files, there were two terms that were used to refer to **'room'**: + - 'salon' (149 times used) + +  + + - 'salle' (46 times used) + +  + + The problem is that both were used in the same context and sometimes even in the same option list. + However, since 'salon' is a better translation and was also in the majority, I used the translation 'salon' wherever 'salle' was marked. + + For example:  - Maximum 25 channels can be loaded in the teams' channels list ([#21708](https://github.com/RocketChat/Rocket.Chat/pull/21708) by [@Jeanstaquet](https://github.com/Jeanstaquet)) @@ -12115,8 +12043,8 @@ - No warning message is sent when user is removed from a team's main channel ([#21949](https://github.com/RocketChat/Rocket.Chat/pull/21949)) - - Send a warning message to a team's main channel when a user is removed from the team; - - Trigger events while removing a user from a team's main channel; + - Send a warning message to a team's main channel when a user is removed from the team; + - Trigger events while removing a user from a team's main channel; - Fix `usersCount` field in the team's main room when a user is removed from the team (`usersCount` is now decreased by 1). - Not possible accept video call if "Hide right sidebar with click" is enabled ([#22175](https://github.com/RocketChat/Rocket.Chat/pull/22175)) @@ -12137,14 +12065,14 @@ - Prevent the userInfo tab to return 'User not found' each time if a certain member of a DM group has been deleted ([#21970](https://github.com/RocketChat/Rocket.Chat/pull/21970) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Prevent the userInfo tab to return 'User not found' if a member of a DM group has been deleted. - This happens if the user that has been deleted is the one originally displayed on the userInfo tab in a DM group with >2 users. - + Prevent the userInfo tab to return 'User not found' if a member of a DM group has been deleted. + This happens if the user that has been deleted is the one originally displayed on the userInfo tab in a DM group with >2 users. + https://user-images.githubusercontent.com/45966964/117221081-db785580-ae08-11eb-9b33-2314a99eb037.mp4 - Prune messages not cleaning up unread threads ([#21326](https://github.com/RocketChat/Rocket.Chat/pull/21326) by [@renancleyson-dev](https://github.com/renancleyson-dev)) - Fixes permanent unread messages when admin prune at least two different thread messages in the room that were unread by some user. + Fixes permanent unread messages when admin prune at least two different thread messages in the room that were unread by some user.  - Redirect on remove user from channel by user profile tab ([#21951](https://github.com/RocketChat/Rocket.Chat/pull/21951)) @@ -12155,8 +12083,8 @@ - Removed fields from User Info for which the user doesn't have permissions. ([#20923](https://github.com/RocketChat/Rocket.Chat/pull/20923) by [@Darshilp326](https://github.com/Darshilp326)) - Removed LastLogin, CreatedAt and Roles for users who don't have permission. - + Removed LastLogin, CreatedAt and Roles for users who don't have permission. + https://user-images.githubusercontent.com/55157259/109381351-f2c62e80-78ff-11eb-9289-e11072bf62f8.mp4 - Replace `query` param by `name`, `username` and `status` on the `teams.members` endpoint ([#21539](https://github.com/RocketChat/Rocket.Chat/pull/21539)) @@ -12169,39 +12097,39 @@ - Unable to edit a 'direct' room setting in the admin due to the room name ([#21636](https://github.com/RocketChat/Rocket.Chat/pull/21636) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - When you are in the admin and want to change a room 'd' setting, it doesn't work because it takes into account the name that is set automatically and therefore tries to save that name. Since the name is not valid and should not be registered, we cannot change the setting for the 'd' room. - I made sure that when you want to change a setting in a 'd' room, that you don't take the name into account - - - https://user-images.githubusercontent.com/45966964/115150919-cd85af00-a06a-11eb-9667-ef3dcfc5adb6.mp4 - - + When you are in the admin and want to change a room 'd' setting, it doesn't work because it takes into account the name that is set automatically and therefore tries to save that name. Since the name is not valid and should not be registered, we cannot change the setting for the 'd' room. + I made sure that when you want to change a setting in a 'd' room, that you don't take the name into account + + + https://user-images.githubusercontent.com/45966964/115150919-cd85af00-a06a-11eb-9667-ef3dcfc5adb6.mp4 + + Behind the scene, the name is not saved - Unable to edit a user who does not have an email via the admin or via the user's profile ([#21626](https://github.com/RocketChat/Rocket.Chat/pull/21626) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - If a user does not have an email address, they cannot change it via their profile or via the admin. I fixed this issue. I have created several profiles and there was one that didn't have an email, I don't know how I did it, I am working on it. I had not modified the db to delete his email, hence the fix - - in admin - - https://user-images.githubusercontent.com/45966964/115112617-9b9b1c80-9f86-11eb-8e3a-950c3c1a1746.mp4 - - - - in the user profile - + If a user does not have an email address, they cannot change it via their profile or via the admin. I fixed this issue. I have created several profiles and there was one that didn't have an email, I don't know how I did it, I am working on it. I had not modified the db to delete his email, hence the fix + + in admin + + https://user-images.githubusercontent.com/45966964/115112617-9b9b1c80-9f86-11eb-8e3a-950c3c1a1746.mp4 + + + + in the user profile + https://user-images.githubusercontent.com/45966964/115112620-a0f86700-9f86-11eb-97b1-56eaba42216b.mp4 - Unable to get channels, sort by most recent message ([#21701](https://github.com/RocketChat/Rocket.Chat/pull/21701) by [@sumukhah](https://github.com/sumukhah)) - Unable to update app manually ([#21215](https://github.com/RocketChat/Rocket.Chat/pull/21215)) - It allows for update of apps using a zip file. - - When installing apps using the zip file, either by url or the file form, if the app was already installed, an error would be thrown stating the condition and forbidding the installation. Now, when sending a zip file of an app that is already installed, the user is presented with the following modal: - -  - + It allows for update of apps using a zip file. + + When installing apps using the zip file, either by url or the file form, if the app was already installed, an error would be thrown stating the condition and forbidding the installation. Now, when sending a zip file of an app that is already installed, the user is presented with the following modal: + +  + If the app also requires permissions to be reviewed, the modal that handles permission reviews will be shown after this one is accepted. - Unpin message reactivity ([#22029](https://github.com/RocketChat/Rocket.Chat/pull/22029)) @@ -12212,20 +12140,20 @@ - User Impersonation through sendMessage API ([#20391](https://github.com/RocketChat/Rocket.Chat/pull/20391) by [@lucassartor](https://github.com/lucassartor)) - Create a new permission: `message-impersonate`. For new installs only bot role will have the permission and for updating installs the permission will also be given to user role, so it won't break running deployments. - - If a message is being sent with `avatar` or `alias` properties, it validates if the sender has the `message-impersonate` permission, if not, an error is throwed: - ```json - { - "success": false, - "error": "Not enough permission", - "stack": "Error: Not enough permission\n ..." - } + Create a new permission: `message-impersonate`. For new installs only bot role will have the permission and for updating installs the permission will also be given to user role, so it won't break running deployments. + + If a message is being sent with `avatar` or `alias` properties, it validates if the sender has the `message-impersonate` permission, if not, an error is throwed: + ```json + { + "success": false, + "error": "Not enough permission", + "stack": "Error: Not enough permission\n ..." + } ``` - Visibility of burger menu on certain width ([#20736](https://github.com/RocketChat/Rocket.Chat/pull/20736)) - Burger was not visible on a certain width, specifically between 600 to 780. if width is more than 780px sidebar is shown, if less than 600 then burger icon was shown. But it wasn't shown between 600px to 780 px. + Burger was not visible on a certain width, specifically between 600 to 780. if width is more than 780px sidebar is shown, if less than 600 then burger icon was shown. But it wasn't shown between 600px to 780 px. It was because for showing burger icon we were only checking for `isMobile` which is lenght only less than 600. So i added one more check for condition if length is less than 780 px. - When closing chats a comment is always required ([#21947](https://github.com/RocketChat/Rocket.Chat/pull/21947)) @@ -12240,8 +12168,8 @@ - Wrong icon on "Move to team" option in the channel info actions ([#21944](https://github.com/RocketChat/Rocket.Chat/pull/21944)) -  - +  + Depends on https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/444 <details> @@ -12258,10 +12186,8 @@ - Add two more test cases to the slash-command test suite ([#21317](https://github.com/RocketChat/Rocket.Chat/pull/21317) by [@EduardoPicolo](https://github.com/EduardoPicolo)) - Added two more test cases to the slash-command test suite: - - - 'should return an error when the command does not exist''; - + Added two more test cases to the slash-command test suite: + - 'should return an error when the command does not exist''; - 'should return an error when no command is provided'; - Bump actions/stale from v3.0.8 to v3.0.18 ([#21877](https://github.com/RocketChat/Rocket.Chat/pull/21877) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -12296,9 +12222,9 @@ - i18n: Add missing translation string in account preference ([#21448](https://github.com/RocketChat/Rocket.Chat/pull/21448) by [@sumukhah](https://github.com/sumukhah)) - "Test Desktop Notifications" was missing in translation, Added to the file. - <img width="691" alt="Screenshot 2021-04-05 at 3 58 01 PM" src="https://user-images.githubusercontent.com/23723464/113565830-475c7800-9629-11eb-8d93-3c177b9d0030.png"> - + "Test Desktop Notifications" was missing in translation, Added to the file. + <img width="691" alt="Screenshot 2021-04-05 at 3 58 01 PM" src="https://user-images.githubusercontent.com/23723464/113565830-475c7800-9629-11eb-8d93-3c177b9d0030.png"> + <img width="701" alt="Screenshot 2021-04-05 at 3 58 32 PM" src="https://user-images.githubusercontent.com/23723464/113565823-44fa1e00-9629-11eb-9af1-839f42e132ca.png"> - i18n: Correct a typo in German ([#21711](https://github.com/RocketChat/Rocket.Chat/pull/21711) by [@Jeanstaquet](https://github.com/Jeanstaquet)) @@ -12325,10 +12251,10 @@ - Regression: discussions display on sidebar ([#22157](https://github.com/RocketChat/Rocket.Chat/pull/22157)) - ### group by type active -  - - ### group by type inactive + ### group by type active +  + + ### group by type inactive  - regression: fix departments with empty ancestors not being returned ([#22068](https://github.com/RocketChat/Rocket.Chat/pull/22068)) @@ -12339,8 +12265,8 @@ - regression: Fix Users list in the Administration ([#22034](https://github.com/RocketChat/Rocket.Chat/pull/22034) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - The app crashed if no custom fields for user profiles have been created by the admin. I fixed this issue. This bug was introduced by a recent commit. - + The app crashed if no custom fields for user profiles have been created by the admin. I fixed this issue. This bug was introduced by a recent commit. + https://user-images.githubusercontent.com/45966964/118210838-5b3a9b80-b46b-11eb-9fe5-5b813848190c.mp4 - Regression: Improve migration 225 ([#22099](https://github.com/RocketChat/Rocket.Chat/pull/22099)) @@ -12357,7 +12283,7 @@ - Regression: not allowed to edit roles due to a new verification ([#22159](https://github.com/RocketChat/Rocket.Chat/pull/22159)) - introduced by https://github.com/RocketChat/Rocket.Chat/pull/21905 + introduced by https://github.com/RocketChat/Rocket.Chat/pull/21905  - regression: Select Team Modal margin ([#22030](https://github.com/RocketChat/Rocket.Chat/pull/22030)) @@ -12368,10 +12294,10 @@ - Regression: Visual issue on sort list item ([#22158](https://github.com/RocketChat/Rocket.Chat/pull/22158)) - ### before -  - - ### after + ### before +  + + ### after  - Release 3.14.2 ([#22135](https://github.com/RocketChat/Rocket.Chat/pull/22135)) @@ -12557,12 +12483,12 @@ - Paginated and Filtered selects on new/edit unit ([#22052](https://github.com/RocketChat/Rocket.Chat/pull/22052) by [@rafaelblink](https://github.com/rafaelblink)) - REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 - - Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well - -  - + REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 + + Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well + +  + This Affects the monitors and departments inputs ### 🚀 Improvements @@ -12638,24 +12564,18 @@ - New set of rules for client code ([#21318](https://github.com/RocketChat/Rocket.Chat/pull/21318)) - This _small_ PR does the following: - - - - Now **React** is the web client's first-class citizen, being **loaded before Blaze**. Thus, `BlazeLayout` calls render templates inside of a React component (`BlazeLayoutWrapper`); - - - Main client startup code, including polyfills, is written in **TypeScript**; - - - At the moment, routes are treated as regular startup code; it's expected that `FlowRouter` will be deprecated in favor of a new routing library; - - - **React** was updated to major version **17**, deprecating the usage of `React` as namespace (e.g. use `memo()` instead of `React.memo()`); - - - The `client/` and `ee/client/` directory are linted with a **custom ESLint configuration** that includes: - - **Prettier**; - - `react-hooks/*` rules for TypeScript files; - - `react/no-multi-comp`, enforcing the rule of **one single React component per module**; - - `react/display-name`, which enforces that **React components must have a name for debugging**; - - `import/named`, avoiding broken named imports. - + This _small_ PR does the following: + + - Now **React** is the web client's first-class citizen, being **loaded before Blaze**. Thus, `BlazeLayout` calls render templates inside of a React component (`BlazeLayoutWrapper`); + - Main client startup code, including polyfills, is written in **TypeScript**; + - At the moment, routes are treated as regular startup code; it's expected that `FlowRouter` will be deprecated in favor of a new routing library; + - **React** was updated to major version **17**, deprecating the usage of `React` as namespace (e.g. use `memo()` instead of `React.memo()`); + - The `client/` and `ee/client/` directory are linted with a **custom ESLint configuration** that includes: + - **Prettier**; + - `react-hooks/*` rules for TypeScript files; + - `react/no-multi-comp`, enforcing the rule of **one single React component per module**; + - `react/display-name`, which enforces that **React components must have a name for debugging**; + - `import/named`, avoiding broken named imports. - A bunch of components were refactored to match the new ESLint rules. - On Hold system messages ([#21360](https://github.com/RocketChat/Rocket.Chat/pull/21360) by [@rafaelblink](https://github.com/rafaelblink)) @@ -12664,15 +12584,12 @@ - Password history ([#21607](https://github.com/RocketChat/Rocket.Chat/pull/21607)) - - Store each user's previously used passwords in a `passwordHistory` field (in the `users` record); - - - Users' previously used passwords are stored in their `passwordHistory` even when the setting is disabled; - - - Add "Password History" setting -- when enabled, it blocks users from reusing their most recent passwords; - - - Convert `comparePassword` file to TypeScript. - -  + - Store each user's previously used passwords in a `passwordHistory` field (in the `users` record); + - Users' previously used passwords are stored in their `passwordHistory` even when the setting is disabled; + - Add "Password History" setting -- when enabled, it blocks users from reusing their most recent passwords; + - Convert `comparePassword` file to TypeScript. + +   - REST endpoint `teams.update` ([#21134](https://github.com/RocketChat/Rocket.Chat/pull/21134) by [@g-thome](https://github.com/g-thome)) @@ -12690,18 +12607,14 @@ - Add error messages to the creation of channels or usernames containing reserved words ([#21016](https://github.com/RocketChat/Rocket.Chat/pull/21016)) - Display error messages when the user attempts to create or edit users' or channels' names with any of the following words (**case-insensitive**): - - - admin; - - - administrator; - - - system; - - - user. -  -  -  + Display error messages when the user attempts to create or edit users' or channels' names with any of the following words (**case-insensitive**): + - admin; + - administrator; + - system; + - user. +  +  +   - add permission check when adding a channel to a team ([#21689](https://github.com/RocketChat/Rocket.Chat/pull/21689) by [@g-thome](https://github.com/g-thome)) @@ -12726,8 +12639,7 @@ - Resize custom emojis on upload instead of saving at max res ([#21593](https://github.com/RocketChat/Rocket.Chat/pull/21593)) - - Create new MediaService (ideally, should be in charge of all media-related operations) - + - Create new MediaService (ideally, should be in charge of all media-related operations) - Resize emojis to 128x128 ### 🛠Bug fixes @@ -12747,25 +12659,25 @@ - Allows more than 25 discussions/files to be loaded in the contextualbar ([#21511](https://github.com/RocketChat/Rocket.Chat/pull/21511) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - In some places, you could not load more than 25 threads/discussions/files on the screen when searching the lists in the contextualbar. - Threads & list are numbered for a better view of the solution - - + In some places, you could not load more than 25 threads/discussions/files on the screen when searching the lists in the contextualbar. + Threads & list are numbered for a better view of the solution + + https://user-images.githubusercontent.com/45966964/114222225-93335800-996e-11eb-833f-568e83129aae.mp4 - Allows more than 25 threads to be loaded, fixes #21507 ([#21508](https://github.com/RocketChat/Rocket.Chat/pull/21508) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Allows to display more than 25 users maximum in the users list ([#21518](https://github.com/RocketChat/Rocket.Chat/pull/21518) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Now when you scroll to the bottom of the users list, it shows more users. Before the fix, the limit for the query for loadMore was calculated so that no additional users could be loaded. - - Before - - https://user-images.githubusercontent.com/45966964/114249739-baece500-999b-11eb-9bb0-3a5bcee18ad8.mp4 - - After - - + Now when you scroll to the bottom of the users list, it shows more users. Before the fix, the limit for the query for loadMore was calculated so that no additional users could be loaded. + + Before + + https://user-images.githubusercontent.com/45966964/114249739-baece500-999b-11eb-9bb0-3a5bcee18ad8.mp4 + + After + + https://user-images.githubusercontent.com/45966964/114249895-364e9680-999c-11eb-985c-47aedc763488.mp4 - App installation from marketplace not correctly displaying the permissions ([#21470](https://github.com/RocketChat/Rocket.Chat/pull/21470)) @@ -12832,19 +12744,19 @@ - Margins on contextual bar information ([#21457](https://github.com/RocketChat/Rocket.Chat/pull/21457)) - ### Room - **Before** -  - - **After** -  - - ### Livechat + ### Room + **Before** +  + + **After** +  + + ### Livechat  - Message Block ordering ([#21464](https://github.com/RocketChat/Rocket.Chat/pull/21464)) - Reactions should come before reply button. + Reactions should come before reply button.  - Message link null corrupts message rendering ([#21579](https://github.com/RocketChat/Rocket.Chat/pull/21579) by [@g-thome](https://github.com/g-thome)) @@ -12897,19 +12809,15 @@ - Typos/missing elements in the French translation ([#21525](https://github.com/RocketChat/Rocket.Chat/pull/21525) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - - I have corrected some typos in the translation - - - I added a translation for missing words - - - I took the opportunity to correct a mistranslated word - - - Test_Desktop_Notifications was missing in the EN and FR file + - I have corrected some typos in the translation + - I added a translation for missing words + - I took the opportunity to correct a mistranslated word + - Test_Desktop_Notifications was missing in the EN and FR file  - Updating a message causing URLs to be parsed even within markdown code ([#21489](https://github.com/RocketChat/Rocket.Chat/pull/21489)) - - Fix `updateMessage` to avoid parsing URLs inside markdown - + - Fix `updateMessage` to avoid parsing URLs inside markdown - Honor `parseUrls` property when updating messages - Use async await in TeamChannels delete channel action ([#21534](https://github.com/RocketChat/Rocket.Chat/pull/21534)) @@ -12922,8 +12830,8 @@ - Wrong user in user info ([#21451](https://github.com/RocketChat/Rocket.Chat/pull/21451)) - Fixed some race conditions in admin. - + Fixed some race conditions in admin. + Self DMs used to be created with the userId duplicated. Sometimes rooms can have 2 equal uids, but it's a self DM. Fixed a getter so this isn't a problem anymore. <details> @@ -12932,30 +12840,22 @@ - Doc: Corrected links to documentation of rocket.chat README.md ([#20478](https://github.com/RocketChat/Rocket.Chat/pull/20478) by [@joshi008](https://github.com/joshi008)) - The link for documentation in the readme was previously https://rocket.chat/docs/ while that was not working and according to the website it was https://docs.rocket.chat/ - The link for deployment methods in readme was corrected from https://rocket.chat/docs/installation/paas-deployments/ to https://docs.rocket.chat/installation/paas-deployments + The link for documentation in the readme was previously https://rocket.chat/docs/ while that was not working and according to the website it was https://docs.rocket.chat/ + The link for deployment methods in readme was corrected from https://rocket.chat/docs/installation/paas-deployments/ to https://docs.rocket.chat/installation/paas-deployments Some more links to the documentations were giving 404 error which hence updated. - [Improve] Remove useless tabbar options from Omnichannel rooms ([#21561](https://github.com/RocketChat/Rocket.Chat/pull/21561) by [@rafaelblink](https://github.com/rafaelblink)) - A React-based replacement for BlazeLayout ([#21527](https://github.com/RocketChat/Rocket.Chat/pull/21527)) - - The Meteor package **`kadira:blaze-layout` was removed**; - - - A **global subscription** for the current application layout (**`appLayout`**) replaces `BlazeLayout` entirely; - - - The **`#react-root` element** is rendered on server-side instead of dynamically injected into the DOM tree; - - - The **"page loading" throbber** is now rendered on the React tree; - - - The **`renderRouteComponent` helper was removed**; - - - Some code run without any criteria on **`main` template** module was moved into **client startup modules**; - - - React portals used to embed Blaze templates have their own subscription (**`blazePortals`**); - - - Some **route components were refactored** to remove a URL path trap originally disabled by `renderRouteComponent`; - + - The Meteor package **`kadira:blaze-layout` was removed**; + - A **global subscription** for the current application layout (**`appLayout`**) replaces `BlazeLayout` entirely; + - The **`#react-root` element** is rendered on server-side instead of dynamically injected into the DOM tree; + - The **"page loading" throbber** is now rendered on the React tree; + - The **`renderRouteComponent` helper was removed**; + - Some code run without any criteria on **`main` template** module was moved into **client startup modules**; + - React portals used to embed Blaze templates have their own subscription (**`blazePortals`**); + - Some **route components were refactored** to remove a URL path trap originally disabled by `renderRouteComponent`; - A new component to embed the DOM nodes generated by **`RoomManager`** was created. - Add ')' after Date and Time in DB migration ([#21519](https://github.com/RocketChat/Rocket.Chat/pull/21519) by [@im-adithya](https://github.com/im-adithya)) @@ -12978,8 +12878,8 @@ - Chore: Meteor update to 2.1.1 ([#21494](https://github.com/RocketChat/Rocket.Chat/pull/21494)) - Basically Node update to version 12.22.1 - + Basically Node update to version 12.22.1 + Meteor change log https://github.com/meteor/meteor/blob/devel/History.md#v211-2021-04-06 - Chore: Remove control character from room model operation ([#21493](https://github.com/RocketChat/Rocket.Chat/pull/21493)) @@ -12988,8 +12888,7 @@ - Fix: Missing module `eventemitter3` for micro services ([#21611](https://github.com/RocketChat/Rocket.Chat/pull/21611)) - - Fix error when running micro services after version 3.12 - + - Fix error when running micro services after version 3.12 - Fix build of docker image version latest for micro services - Language update from LingoHub 🤖 on 2021-04-05Z ([#21446](https://github.com/RocketChat/Rocket.Chat/pull/21446)) @@ -13002,12 +12901,9 @@ - QoL improvements to add channel to team flow ([#21778](https://github.com/RocketChat/Rocket.Chat/pull/21778)) - - Fixed canAccessRoom validation - - - Added e2e tests - - - Removed channels that user cannot add to the team from autocomplete suggestions - + - Fixed canAccessRoom validation + - Added e2e tests + - Removed channels that user cannot add to the team from autocomplete suggestions - Improved error messages - Regression: Bold, italic and strike render (Original markdown) ([#21747](https://github.com/RocketChat/Rocket.Chat/pull/21747)) @@ -13030,10 +12926,10 @@ - Regression: Legacy Banner Position ([#21598](https://github.com/RocketChat/Rocket.Chat/pull/21598)) - ### Before: -  - - ### After + ### Before: +  + + ### After  - regression: Markdown broken on safari ([#21780](https://github.com/RocketChat/Rocket.Chat/pull/21780)) @@ -13243,62 +13139,56 @@ - **APPS:** New event interfaces for pre/post user leaving a room ([#20917](https://github.com/RocketChat/Rocket.Chat/pull/20917) by [@lucassartor](https://github.com/lucassartor)) - Added events and errors that trigger when a user leaves a room. + Added events and errors that trigger when a user leaves a room. That way it can communicate with the Apps-Engine by the `IPreRoomUserLeave` and `IPostRoomUserLeave` event interfaces. - **Enterprise:** Omnichannel On-Hold Queue ([#20945](https://github.com/RocketChat/Rocket.Chat/pull/20945)) - ### About this feature - This feature has been introduced to deal with Inactive chats. A chat is considered Inactive if an Omnichannel End User (aka Visitor) has not replied back to an agent in some time. These types of inactive chats become very important when an organisation has a limit set for `Max Simultaneous Chats per agent` which is defined by the following setting :point_down: , as more number of Inactive chats would directly affect an agent's productivity. -  - - Before this feature, we only had one option to deal with such Inactive/Abandoned chats - which was to auto close abandoned chats via this setting :point_down: -  - - however closing a chat isn't a best option for some cases. Let me take an example to explain a scenario - - > An agent is assisting a customer for installing a very huge software which is likely to take more than 20-30 minutes to download. In such scenarios closing a chat isn't the best approach since even after the lengthy download the customer might still need some assist from the agent. - > So basically this chat is going to block the agent's queue until the customer is able to finish his time-consuming download task in which he/she doesn't require any agent's assistance. Due to the `Max Simultaneous Chats per agent` limit, the agent is also not able to use this extra time to help other customer thus affecting his overall productivity. - - **So how does the On-Hold feature solve this problem?** - With the On-Hold feature, an agent is now able to place a chat on-hold. On-Hold chats **don’t count towards the maximum number of concurrent chats** an agent can have. So in our above example, the agent can simply now place the customer on-hold for 20-30 minutes until the customer downloads the software and within this time, the agent can serve other customers - hence increasing the productivity of an agent. - - ---------------------------------------- - ### Working of the new On-Hold feature - - #### How can you place a chat on Hold ? - - A chat can be placed on-hold via 2 means - - 1. Automatically place Abandoned chats On-hold -  - Via this :top: option you can define a timer which will get started when a customer sends a message. If we don't receive any message from the customer within this timer, the timer will get expired and the chat will be considered as Abandoned. -  - The via this :top: setting you can choose to automatically place this abandoned chat On Hold - - 2. Manually place a chat On Hold - As an admin, you can allow an agent to manually place a chat on-hold. To do so, you'll need to turn on this :point_down: setting -  - Now an agent will be able to see a new `On Hold` button within their `Visitor Info Panel` like this :point_down: , provided the agent has sent the last message -  - - #### How can you resume a On Hold chat ? - An On Hold chat can be resumed via 2 means - - - 1. If the Customer sends a message - If the Customer / Omnichannel End User sends a message to the On Hold chat, the On Hold chat will get automatically resumed. - - 2. Manually by agent - An Agent can manually resume the On Hold chat via clicking the `Resume` button in the bottom of a chat room. -  - - #### What would happen if the agent already reached maximum chats, and a On-Hold chat gets resumed ? - Based on how the chat was resumed, there are multiple cases are each case is dealt differently - - - - If an agent manually tries to resume the On Hold chat, he/she will get an error saying `Maximum Simultaneous chat limit reached` - + ### About this feature + This feature has been introduced to deal with Inactive chats. A chat is considered Inactive if an Omnichannel End User (aka Visitor) has not replied back to an agent in some time. These types of inactive chats become very important when an organisation has a limit set for `Max Simultaneous Chats per agent` which is defined by the following setting :point_down: , as more number of Inactive chats would directly affect an agent's productivity. +  + + Before this feature, we only had one option to deal with such Inactive/Abandoned chats - which was to auto close abandoned chats via this setting :point_down: +  + + however closing a chat isn't a best option for some cases. Let me take an example to explain a scenario + + > An agent is assisting a customer for installing a very huge software which is likely to take more than 20-30 minutes to download. In such scenarios closing a chat isn't the best approach since even after the lengthy download the customer might still need some assist from the agent. + > So basically this chat is going to block the agent's queue until the customer is able to finish his time-consuming download task in which he/she doesn't require any agent's assistance. Due to the `Max Simultaneous Chats per agent` limit, the agent is also not able to use this extra time to help other customer thus affecting his overall productivity. + + **So how does the On-Hold feature solve this problem?** + With the On-Hold feature, an agent is now able to place a chat on-hold. On-Hold chats **don’t count towards the maximum number of concurrent chats** an agent can have. So in our above example, the agent can simply now place the customer on-hold for 20-30 minutes until the customer downloads the software and within this time, the agent can serve other customers - hence increasing the productivity of an agent. + + ---------------------------------------- + ### Working of the new On-Hold feature + + #### How can you place a chat on Hold ? + + A chat can be placed on-hold via 2 means + 1. Automatically place Abandoned chats On-hold +  + Via this :top: option you can define a timer which will get started when a customer sends a message. If we don't receive any message from the customer within this timer, the timer will get expired and the chat will be considered as Abandoned. +  + The via this :top: setting you can choose to automatically place this abandoned chat On Hold + 2. Manually place a chat On Hold + As an admin, you can allow an agent to manually place a chat on-hold. To do so, you'll need to turn on this :point_down: setting +  + Now an agent will be able to see a new `On Hold` button within their `Visitor Info Panel` like this :point_down: , provided the agent has sent the last message +  + + #### How can you resume a On Hold chat ? + An On Hold chat can be resumed via 2 means + + 1. If the Customer sends a message + If the Customer / Omnichannel End User sends a message to the On Hold chat, the On Hold chat will get automatically resumed. + 2. Manually by agent + An Agent can manually resume the On Hold chat via clicking the `Resume` button in the bottom of a chat room. +  + + #### What would happen if the agent already reached maximum chats, and a On-Hold chat gets resumed ? + Based on how the chat was resumed, there are multiple cases are each case is dealt differently + + - If an agent manually tries to resume the On Hold chat, he/she will get an error saying `Maximum Simultaneous chat limit reached` - If a customer replies back on an On Hold chat and the last serving agent has reached maximum capacity, then this customer will be placed on the queue again from where based on the Routing Algorithm selected, the chat will get transferred to any available agent - Ability to hide 'Room topic changed' system messages ([#21062](https://github.com/RocketChat/Rocket.Chat/pull/21062) by [@Tirieru](https://github.com/Tirieru)) @@ -13309,39 +13199,33 @@ - Teams ([#20966](https://github.com/RocketChat/Rocket.Chat/pull/20966) by [@g-thome](https://github.com/g-thome)) - ## Teams - - - - You can easily group your users as Teams on Rocket.Chat. The feature takes the hassle out of managing multiple users one by one and allows you to handle them at the same time efficiently. - - - - - Teams can be public or private and each team can have its own channels, which also can be public or private. - - - It's possible to add existing channels to a Team or create new ones inside a Team. - - - It's possible to invite people outside a Team to join Team's channels. - - - It's possible to convert channels to Teams - - - It's possible to add all team members to a channel at once - - - Team members have roles - - -  - - - - **Quickly onboard new users with Autojoin channels** - - Teams can have Auto-join channels – channels to which the team members are automatically added, so you don’t need to go through the manual process of adding users repetitively - -  - - **Instantly mention multiple members at once** (available in EE) - + ## Teams + + + + You can easily group your users as Teams on Rocket.Chat. The feature takes the hassle out of managing multiple users one by one and allows you to handle them at the same time efficiently. + + + - Teams can be public or private and each team can have its own channels, which also can be public or private. + - It's possible to add existing channels to a Team or create new ones inside a Team. + - It's possible to invite people outside a Team to join Team's channels. + - It's possible to convert channels to Teams + - It's possible to add all team members to a channel at once + - Team members have roles + + +  + + + + **Quickly onboard new users with Autojoin channels** + + Teams can have Auto-join channels – channels to which the team members are automatically added, so you don’t need to go through the manual process of adding users repetitively + +  + + **Instantly mention multiple members at once** (available in EE) + With Teams, you don’t need to remember everyone’s name to communicate with a team quickly. Just mention a Team — @engineers, for instance — and all members will be instantly notified. ### 🚀 Improvements @@ -13351,22 +13235,22 @@ - Added modal-box for preview after recording audio. ([#20370](https://github.com/RocketChat/Rocket.Chat/pull/20370) by [@Darshilp326](https://github.com/Darshilp326)) - A modal box will be displayed so that users can change the filename and add description. - - **Before** - - https://user-images.githubusercontent.com/55157259/105687301-4e2a8880-5f1e-11eb-873d-dc8a880a2fc8.mp4 - - **After** - + A modal box will be displayed so that users can change the filename and add description. + + **Before** + + https://user-images.githubusercontent.com/55157259/105687301-4e2a8880-5f1e-11eb-873d-dc8a880a2fc8.mp4 + + **After** + https://user-images.githubusercontent.com/55157259/105687342-597db400-5f1e-11eb-8b61-8f9d9ebad0c4.mp4 - Adds toast after follow/unfollow messages and following icon for followed messages without threads. ([#20025](https://github.com/RocketChat/Rocket.Chat/pull/20025) by [@RonLek](https://github.com/RonLek)) - There was no alert on following/unfollowing a message previously. Also, it was impossible to make out a followed message with no threads from an unfollowed one. - - This PR would show an alert on following/unfollowing a message and also display a small bell icon (similar to the ones for starred and pinned messages) when a message with no thread is followed. - + There was no alert on following/unfollowing a message previously. Also, it was impossible to make out a followed message with no threads from an unfollowed one. + + This PR would show an alert on following/unfollowing a message and also display a small bell icon (similar to the ones for starred and pinned messages) when a message with no thread is followed. + https://user-images.githubusercontent.com/28918901/103813540-43e73e00-5086-11eb-8592-2877eb650f3e.mp4 - Back to threads list button on threads contextual bar ([#20882](https://github.com/RocketChat/Rocket.Chat/pull/20882)) @@ -13379,12 +13263,12 @@ - Improve Apps permission modal ([#21193](https://github.com/RocketChat/Rocket.Chat/pull/21193) by [@lucassartor](https://github.com/lucassartor)) - Improve the UI of the Apps permission modal when installing an App that requires permissions. - - **New UI:** -  - - **Old UI:** + Improve the UI of the Apps permission modal when installing an App that requires permissions. + + **New UI:** +  + + **Old UI:**  - Make debug logs of Apps configurable via Log_Level setting in the Admin panel ([#21000](https://github.com/RocketChat/Rocket.Chat/pull/21000) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) @@ -13395,15 +13279,15 @@ - Sort Users List In Case Insensitive Manner ([#20790](https://github.com/RocketChat/Rocket.Chat/pull/20790) by [@aditya-mitra](https://github.com/aditya-mitra)) - The users listed in the admin panel were sorted in a case-sensitive manner , where the capitals came first and then the small letters (like - *A B C a b c*). This Change fixes this by sorting the names in a caseinsensitive manner (now - *A a B b C c*). - - ### Before - -  - - - ### With This Change - + The users listed in the admin panel were sorted in a case-sensitive manner , where the capitals came first and then the small letters (like - *A B C a b c*). This Change fixes this by sorting the names in a caseinsensitive manner (now - *A a B b C c*). + + ### Before + +  + + + ### With This Change +  ### 🛠Bug fixes @@ -13417,12 +13301,12 @@ - **APPS:** Warn message while installing app in air-gapped environment ([#20992](https://github.com/RocketChat/Rocket.Chat/pull/20992) by [@lucassartor](https://github.com/lucassartor)) - Change **error** message to a **warn** message when uploading a `.zip` file app into a air-gapped environment. - - The **error** message was giving the impression for the user that the app wasn't properly being installed , which it wasn't the case: -  - - A more detailed **warn** message can fix that impression for the user: + Change **error** message to a **warn** message when uploading a `.zip` file app into a air-gapped environment. + + The **error** message was giving the impression for the user that the app wasn't properly being installed , which it wasn't the case: +  + + A more detailed **warn** message can fix that impression for the user:  - Add missing `unreads` field to `users.info` REST endpoint ([#20905](https://github.com/RocketChat/Rocket.Chat/pull/20905)) @@ -13437,10 +13321,10 @@ - Correct direction for admin mapview text ([#20897](https://github.com/RocketChat/Rocket.Chat/pull/20897) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) -  -  -  - +  +  +  + The text says the share button will be on the left of the messagebox once enabled. However, it actually is on the right. - Correct ignored message CSS ([#20928](https://github.com/RocketChat/Rocket.Chat/pull/20928) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -13457,13 +13341,13 @@ - Custom emojis to override default ([#20359](https://github.com/RocketChat/Rocket.Chat/pull/20359) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Due to the sequence of the imports and how the emojiRenderer prioritizes lists, the custom emojis could not override the emojione emojis. Making two small changes fixed the issue. - - With the custom emoji for `:facepalm:` added, you can check out the result below: - ### Before -  - - ### After + Due to the sequence of the imports and how the emojiRenderer prioritizes lists, the custom emojis could not override the emojione emojis. Making two small changes fixed the issue. + + With the custom emoji for `:facepalm:` added, you can check out the result below: + ### Before +  + + ### After  - Empty URL in user avatar doesn't show error and enables save ([#20440](https://github.com/RocketChat/Rocket.Chat/pull/20440) by [@im-adithya](https://github.com/im-adithya)) @@ -13476,12 +13360,12 @@ - Fix the search list showing the last channel ([#21160](https://github.com/RocketChat/Rocket.Chat/pull/21160) by [@shrinish123](https://github.com/shrinish123)) - The search list now also properly shows the last channel - Before : - -  - - After : + The search list now also properly shows the last channel + Before : + +  + + After :  - Follow thread action on threads list ([#20881](https://github.com/RocketChat/Rocket.Chat/pull/20881)) @@ -13506,13 +13390,13 @@ - Multi Select isn't working in Export Messages ([#21236](https://github.com/RocketChat/Rocket.Chat/pull/21236) by [@PriyaBihani](https://github.com/PriyaBihani)) - While exporting messages, we were not able to select multiple Users like this: - - https://user-images.githubusercontent.com/69837339/111953057-169a2000-8b0c-11eb-94a4-0e1657683f96.mp4 - - Now we can select multiple users: - - + While exporting messages, we were not able to select multiple Users like this: + + https://user-images.githubusercontent.com/69837339/111953057-169a2000-8b0c-11eb-94a4-0e1657683f96.mp4 + + Now we can select multiple users: + + https://user-images.githubusercontent.com/69837339/111953097-274a9600-8b0c-11eb-9177-bec388b042bd.mp4 - New Channel popover not closing ([#21080](https://github.com/RocketChat/Rocket.Chat/pull/21080)) @@ -13521,31 +13405,31 @@ - OEmbedURLWidget - Show Full Embedded Text Description ([#20569](https://github.com/RocketChat/Rocket.Chat/pull/20569) by [@aditya-mitra](https://github.com/aditya-mitra)) - Embeds were cutoff when either _urls had a long description_. - This was handled by removing `overflow:hidden;text-overflow:ellipsis;` from the inline styles in [`oembedUrlWidget.html`](https://github.com/RocketChat/Rocket.Chat/blob/develop/app/oembed/client/oembedUrlWidget.html#L28). - - ### Earlier - -  - - ### Now - + Embeds were cutoff when either _urls had a long description_. + This was handled by removing `overflow:hidden;text-overflow:ellipsis;` from the inline styles in [`oembedUrlWidget.html`](https://github.com/RocketChat/Rocket.Chat/blob/develop/app/oembed/client/oembedUrlWidget.html#L28). + + ### Earlier + +  + + ### Now +  - Reactions list showing users in reactions option of message action. ([#20753](https://github.com/RocketChat/Rocket.Chat/pull/20753) by [@Darshilp326](https://github.com/Darshilp326)) - Reactions list shows emojis with respected users who have reacted with that emoji. - + Reactions list shows emojis with respected users who have reacted with that emoji. + https://user-images.githubusercontent.com/55157259/107857609-5870e000-6e55-11eb-8137-494a9f71b171.mp4 - Removing truncation from profile ([#20352](https://github.com/RocketChat/Rocket.Chat/pull/20352) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Truncating text in profile view was making some information completely inaccessible. Removed it from the user status and the custom fields where if the information is longer, the user would actually want to see all of it. - - ### Before -  - - ### After + Truncating text in profile view was making some information completely inaccessible. Removed it from the user status and the custom fields where if the information is longer, the user would actually want to see all of it. + + ### Before +  + + ### After  - Replace wrong field description on Room Information panel ([#21395](https://github.com/RocketChat/Rocket.Chat/pull/21395) by [@rafaelblink](https://github.com/rafaelblink)) @@ -13556,8 +13440,8 @@ - Set establishing to false if OTR timeouts ([#21183](https://github.com/RocketChat/Rocket.Chat/pull/21183) by [@Darshilp326](https://github.com/Darshilp326)) - Set establishing false if OTR timeouts. - + Set establishing false if OTR timeouts. + https://user-images.githubusercontent.com/55157259/111617086-b30cab80-8808-11eb-8740-3b4ffacfc322.mp4 - Sidebar scroll missing full height ([#21071](https://github.com/RocketChat/Rocket.Chat/pull/21071)) @@ -13596,33 +13480,20 @@ - Chore: Add tests for Meteor methods ([#20901](https://github.com/RocketChat/Rocket.Chat/pull/20901)) - Add end-to-end tests for the following meteor methods - - - - [x] public-settings:get - - - [x] rooms:get - - - [x] subscriptions:get - - - [x] permissions:get - - - [x] loadMissedMessages - - - [x] loadHistory - - - [x] listCustomUserStatus - - - [x] getUserRoles - - - [x] getRoomRoles (called by the API, already covered) - - - [x] getMessages - - - [x] getUsersOfRoom - - - [x] loadNextMessages - + Add end-to-end tests for the following meteor methods + + - [x] public-settings:get + - [x] rooms:get + - [x] subscriptions:get + - [x] permissions:get + - [x] loadMissedMessages + - [x] loadHistory + - [x] listCustomUserStatus + - [x] getUserRoles + - [x] getRoomRoles (called by the API, already covered) + - [x] getMessages + - [x] getUsersOfRoom + - [x] loadNextMessages - [x] getThreadMessages - Chore: Meteor update 2.1 ([#21061](https://github.com/RocketChat/Rocket.Chat/pull/21061)) @@ -13635,10 +13506,8 @@ - Improve: Increase testing coverage ([#21015](https://github.com/RocketChat/Rocket.Chat/pull/21015)) - Add test for - - - settings/raw - + Add test for + - settings/raw - minimongo/comparisons - Improve: NPS survey fetch ([#21263](https://github.com/RocketChat/Rocket.Chat/pull/21263)) @@ -13657,19 +13526,17 @@ - Regression: Add scope to permission checks in Team's endpoints ([#21369](https://github.com/RocketChat/Rocket.Chat/pull/21369)) - - Include scope (team's main room ID) in the permission checks; + - Include scope (team's main room ID) in the permission checks; - Remove the `teamName` parameter from the `members`, `addMembers`, `updateMember` and `removeMembers` methods (since `teamId` will always be defined). - Regression: Add support to filter on `teams.listRooms` endpoint ([#21327](https://github.com/RocketChat/Rocket.Chat/pull/21327)) - - Add support for queries (within the `query` parameter); - + - Add support for queries (within the `query` parameter); - Add support to pagination (`offset` and `count`) when an user doesn't have the permission to get all rooms. - Regression: Add teams support to directory ([#21351](https://github.com/RocketChat/Rocket.Chat/pull/21351)) - - Change `directory.js` to reduce function complexity - + - Change `directory.js` to reduce function complexity - Add `teams` type of item. Directory will return all public teams & private teams the user is part of. - Regression: add view room action on Teams Channels ([#21295](https://github.com/RocketChat/Rocket.Chat/pull/21295)) @@ -13722,19 +13589,18 @@ - Regression: Quick action button missing for Omnichannel On-Hold queue ([#21285](https://github.com/RocketChat/Rocket.Chat/pull/21285)) - - Move the Manual On Hold button to the new Omnichannel Header -  -  - - + - Move the Manual On Hold button to the new Omnichannel Header +  +  + - Minor fixes - regression: Remove Breadcrumbs and update Tag component ([#21399](https://github.com/RocketChat/Rocket.Chat/pull/21399)) - Regression: Remove channel action on add channel's modal don't work ([#21356](https://github.com/RocketChat/Rocket.Chat/pull/21356)) -  - +  +  - Regression: Remove primary color from button in TeamChannels component ([#21293](https://github.com/RocketChat/Rocket.Chat/pull/21293)) @@ -13763,10 +13629,10 @@ - Regression: Unify Contact information displayed on the Room header and Room Info ([#21312](https://github.com/RocketChat/Rocket.Chat/pull/21312) by [@rafaelblink](https://github.com/rafaelblink)) -  - -  - +  + +  +  - Regression: Unify team actions to add a room to a team ([#21386](https://github.com/RocketChat/Rocket.Chat/pull/21386)) @@ -13775,10 +13641,8 @@ - Regression: Update .invite endpoints to support multiple users at once ([#21328](https://github.com/RocketChat/Rocket.Chat/pull/21328)) - - channels.invite now supports passing an array as a param (either with usernames or userIds) via `usernames` or `userIds` properties. - - - You can still use the endpoint to invite only one user via the old params `userId`, `username` or `user`. - + - channels.invite now supports passing an array as a param (either with usernames or userIds) via `usernames` or `userIds` properties. + - You can still use the endpoint to invite only one user via the old params `userId`, `username` or `user`. - Same changes apply to groups.invite - Regression: user actions in admin ([#21307](https://github.com/RocketChat/Rocket.Chat/pull/21307)) @@ -13913,7 +13777,7 @@ - Close Call contextual bar after starting jitsi call. ([#21004](https://github.com/RocketChat/Rocket.Chat/pull/21004)) - After jitsi call is started, if the call is started in a new window then we should close contextual tab bar. + After jitsi call is started, if the call is started in a new window then we should close contextual tab bar. So, when 'YES' is pressed on modal, we call handleClose function if openNewWindow is true, as call doesn't starts on tab bar, it starts on new window. ### 🛠Bug fixes @@ -13923,7 +13787,7 @@ - Stopping Jitsi reload ([#20973](https://github.com/RocketChat/Rocket.Chat/pull/20973)) - The Function where Jitsi call is started gets called many times due to `room.usernames` dep of useMemo, this dep triggers reloading of this function many times. + The Function where Jitsi call is started gets called many times due to `room.usernames` dep of useMemo, this dep triggers reloading of this function many times. So removing this dep from useMemo dependencies ### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 @@ -13948,10 +13812,10 @@ - Cloud Workspace bridge ([#20838](https://github.com/RocketChat/Rocket.Chat/pull/20838)) - Adds the new CloudWorkspace functionality. - - It allows apps to request the access token for the workspace it's installed on, so it can perform actions with other Rocket.Chat services, such as the Omni Gateway. - + Adds the new CloudWorkspace functionality. + + It allows apps to request the access token for the workspace it's installed on, so it can perform actions with other Rocket.Chat services, such as the Omni Gateway. + https://github.com/RocketChat/Rocket.Chat.Apps-engine/pull/382 - Header with Breadcrumbs ([#20609](https://github.com/RocketChat/Rocket.Chat/pull/20609)) @@ -13969,10 +13833,10 @@ - Add symbol to indicate apps' required settings in the UI ([#20447](https://github.com/RocketChat/Rocket.Chat/pull/20447)) - - Apps are able to define **required** settings. These settings should not be left blank by the user and an error will be thrown and shown in the interface if an user attempts to save changes in the app details page leaving any required fields blank; -  - - - A sign (*) is added to the label of app settings' fields that are required so as to highlight the fields which must not be left blank. + - Apps are able to define **required** settings. These settings should not be left blank by the user and an error will be thrown and shown in the interface if an user attempts to save changes in the app details page leaving any required fields blank; +  + + - A sign (*) is added to the label of app settings' fields that are required so as to highlight the fields which must not be left blank.  - Add visual validation on users admin forms ([#20308](https://github.com/RocketChat/Rocket.Chat/pull/20308)) @@ -13993,20 +13857,20 @@ - Adds tooltip for sidebar header icons ([#19934](https://github.com/RocketChat/Rocket.Chat/pull/19934) by [@RonLek](https://github.com/RonLek)) - Previously the header icons in the sidebar didn't show a tooltip when hovered over. This PR fixes that. - + Previously the header icons in the sidebar didn't show a tooltip when hovered over. This PR fixes that. +  - Better Presentation of Blockquotes ([#20750](https://github.com/RocketChat/Rocket.Chat/pull/20750) by [@aditya-mitra](https://github.com/aditya-mitra)) - Changed the values of `margin-top` and `margin-bottom` for *first* and *last* childs in blockquotes to increase readability. - - ### Before - -  - - ### Now - + Changed the values of `margin-top` and `margin-bottom` for *first* and *last* childs in blockquotes to increase readability. + + ### Before + +  + + ### Now +  - Change header based on room type ([#20612](https://github.com/RocketChat/Rocket.Chat/pull/20612)) @@ -14027,18 +13891,13 @@ - Replace react-window for react-virtuoso package ([#20392](https://github.com/RocketChat/Rocket.Chat/pull/20392)) - Remove: - - - react-window - - - react-window-infinite-loader - - - simplebar-react - - Include: - - - react-virtuoso - + Remove: + - react-window + - react-window-infinite-loader + - simplebar-react + + Include: + - react-virtuoso - rc-scrollbars - Rewrite Call as React component ([#19778](https://github.com/RocketChat/Rocket.Chat/pull/19778)) @@ -14054,13 +13913,13 @@ - Add debouncing to add users search field. ([#20297](https://github.com/RocketChat/Rocket.Chat/pull/20297) by [@Darshilp326](https://github.com/Darshilp326)) - BEFORE - - https://user-images.githubusercontent.com/55157259/105350722-98a3c080-5c11-11eb-82f3-d9a62a4fa50b.mp4 - - - AFTER - + BEFORE + + https://user-images.githubusercontent.com/55157259/105350722-98a3c080-5c11-11eb-82f3-d9a62a4fa50b.mp4 + + + AFTER + https://user-images.githubusercontent.com/55157259/105350757-a2c5bf00-5c11-11eb-91db-25c0b9e01a28.mp4 - Add tooltips to Thread header buttons ([#20456](https://github.com/RocketChat/Rocket.Chat/pull/20456) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -14073,8 +13932,8 @@ - Added check for view admin permission page ([#20403](https://github.com/RocketChat/Rocket.Chat/pull/20403)) - Admin Permission page was visible to all, if you add admin/permissions after the base url. This should not be visible to all user, only people with certain permissions should be able to see this page. - I am also able to see permissions page for open workspace of Rocket chat. + Admin Permission page was visible to all, if you add admin/permissions after the base url. This should not be visible to all user, only people with certain permissions should be able to see this page. + I am also able to see permissions page for open workspace of Rocket chat.  - Adding the accidentally deleted tag template, used by other templates ([#20772](https://github.com/RocketChat/Rocket.Chat/pull/20772)) @@ -14083,8 +13942,8 @@ - Admin cannot clear user details like bio or nickname ([#20785](https://github.com/RocketChat/Rocket.Chat/pull/20785)) - When the API users.update is called to update user data, it passes data to saveUser function. Here before saving data like bio or nickname we are checking if they are available or not. If data is available then we are saving it, but we are not doing anything when data isn't available. - + When the API users.update is called to update user data, it passes data to saveUser function. Here before saving data like bio or nickname we are checking if they are available or not. If data is available then we are saving it, but we are not doing anything when data isn't available. + So unsetting data if data isn't available to save. Will also fix bio and other fields. :) - Admin Panel pages not visible in Safari ([#20912](https://github.com/RocketChat/Rocket.Chat/pull/20912)) @@ -14101,24 +13960,24 @@ - Blank Personal Access Token Bug ([#20193](https://github.com/RocketChat/Rocket.Chat/pull/20193) by [@RonLek](https://github.com/RonLek)) - Adds error when personal access token is blank thereby disallowing the creation of one. - + Adds error when personal access token is blank thereby disallowing the creation of one. + https://user-images.githubusercontent.com/28918901/104483631-5adde100-55ee-11eb-9938-64146bce127e.mp4 - CAS login failing due to TOTP requirement ([#20840](https://github.com/RocketChat/Rocket.Chat/pull/20840)) - Changed password input field for password access in edit room info. ([#20356](https://github.com/RocketChat/Rocket.Chat/pull/20356) by [@Darshilp326](https://github.com/Darshilp326)) - Password field would be secured with asterisks in edit room info - - https://user-images.githubusercontent.com/55157259/105641758-cad04f00-5eab-11eb-90de-0c91263edd55.mp4 - + Password field would be secured with asterisks in edit room info + + https://user-images.githubusercontent.com/55157259/105641758-cad04f00-5eab-11eb-90de-0c91263edd55.mp4 + . - Channel mentions showing user subscribed channels twice ([#20484](https://github.com/RocketChat/Rocket.Chat/pull/20484) by [@Darshilp326](https://github.com/Darshilp326)) - Channel mention shows user subscribed channels twice. - + Channel mention shows user subscribed channels twice. + https://user-images.githubusercontent.com/55157259/106183033-b353d780-61c5-11eb-8aab-1dbb62b02ff8.mp4 - CORS config not accepting multiple origins ([#20696](https://github.com/RocketChat/Rocket.Chat/pull/20696) by [@g-thome](https://github.com/g-thome)) @@ -14129,26 +13988,26 @@ - Default Attachments - Remove Extra Margin in Field Attachments ([#20618](https://github.com/RocketChat/Rocket.Chat/pull/20618) by [@aditya-mitra](https://github.com/aditya-mitra)) - A large amount of unnecessary margin which existed in the **Field Attachments inside the `DefaultAttachments`** has been fixed. - - ### Earlier - -  - - ### Now - + A large amount of unnecessary margin which existed in the **Field Attachments inside the `DefaultAttachments`** has been fixed. + + ### Earlier + +  + + ### Now +  - Default Attachments - Show Full Attachment.Text with Markdown ([#20606](https://github.com/RocketChat/Rocket.Chat/pull/20606) by [@aditya-mitra](https://github.com/aditya-mitra)) - Removed truncating of text in `Attachment.Text`. - Added `Attachment.Text` to be parsed to markdown by default. - - ### Earlier -  - - ### Now - + Removed truncating of text in `Attachment.Text`. + Added `Attachment.Text` to be parsed to markdown by default. + + ### Earlier +  + + ### Now +  - Don't ask again not rendering ([#20745](https://github.com/RocketChat/Rocket.Chat/pull/20745)) @@ -14169,21 +14028,21 @@ - Feedback on bulk invite ([#20339](https://github.com/RocketChat/Rocket.Chat/pull/20339) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Resolved structure where no response was being received. Changed from callback to async/await. - Added error in case of empty submission, or if no valid emails were found. - + Resolved structure where no response was being received. Changed from callback to async/await. + Added error in case of empty submission, or if no valid emails were found. + https://user-images.githubusercontent.com/38764067/105613964-dfe5a900-5deb-11eb-80f2-21fc8dee57c0.mp4 - Filters are not being applied correctly in Omnichannel Current Chats list ([#20320](https://github.com/RocketChat/Rocket.Chat/pull/20320) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before -  - - ### After -  - -  - + ### Before +  + + ### After +  + +  +  - Fix Empty highlighted words field ([#20329](https://github.com/RocketChat/Rocket.Chat/pull/20329)) @@ -14212,11 +14071,11 @@ - List of Omnichannel triggers is not listing data ([#20624](https://github.com/RocketChat/Rocket.Chat/pull/20624) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before -  - - - ### After + ### Before +  + + + ### After  - Livechat bridge permission checkers ([#20653](https://github.com/RocketChat/Rocket.Chat/pull/20653) by [@lolimay](https://github.com/lolimay)) @@ -14239,8 +14098,7 @@ - Missing setting to control when to send the ReplyTo field in email notifications ([#20744](https://github.com/RocketChat/Rocket.Chat/pull/20744)) - - Add a new setting ("Add Reply-To header") in the Email settings' page to control when the Reply-To header is used in e-mail notifications; - + - Add a new setting ("Add Reply-To header") in the Email settings' page to control when the Reply-To header is used in e-mail notifications; - The new setting is turned off (`false` value) by default. - New Integration page was not being displayed ([#20670](https://github.com/RocketChat/Rocket.Chat/pull/20670)) @@ -14273,15 +14131,15 @@ - Remove duplicate getCommonRoomEvents() event binding for starredMessages ([#20185](https://github.com/RocketChat/Rocket.Chat/pull/20185) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - The getCommonRoomEvents() returned functions were bound to the starredMessages template twice. This was causing some bugs, as detailed in the Issue mentioned below. + The getCommonRoomEvents() returned functions were bound to the starredMessages template twice. This was causing some bugs, as detailed in the Issue mentioned below. I removed the top events call that only bound the getCommonRoomEvents(). Therefore, only one call for the same is left, which is at the end of the file. Having the events bound just once removes the bugs mentioned. - Remove warning problems from console ([#20800](https://github.com/RocketChat/Rocket.Chat/pull/20800)) - Removed tooltip in kebab menu options. ([#20498](https://github.com/RocketChat/Rocket.Chat/pull/20498) by [@Darshilp326](https://github.com/Darshilp326)) - Removed tooltip as it was not needed. - + Removed tooltip as it was not needed. + https://user-images.githubusercontent.com/55157259/106246146-a53ca000-6233-11eb-9874-cbd1b4331bc0.mp4 - Retry icon comes out of the div ([#20390](https://github.com/RocketChat/Rocket.Chat/pull/20390) by [@im-adithya](https://github.com/im-adithya)) @@ -14296,8 +14154,8 @@ - Room's last message's update date format on IE ([#20680](https://github.com/RocketChat/Rocket.Chat/pull/20680)) - The proposed change fixes a bug when updates the cached records on Internet Explorer and it breaks the sidebar as shown on the screenshot below: - + The proposed change fixes a bug when updates the cached records on Internet Explorer and it breaks the sidebar as shown on the screenshot below: +  - Save user password and email from My Account ([#20737](https://github.com/RocketChat/Rocket.Chat/pull/20737)) @@ -14306,8 +14164,8 @@ - Selected hide system messages would now be viewed in vertical bar. ([#20358](https://github.com/RocketChat/Rocket.Chat/pull/20358) by [@Darshilp326](https://github.com/Darshilp326)) - All selected hide system messages are now in vertical Bar. - + All selected hide system messages are now in vertical Bar. + https://user-images.githubusercontent.com/55157259/105642624-d5411780-5eb0-11eb-8848-93e4b02629cb.mp4 - Selected messages don't get unselected ([#20408](https://github.com/RocketChat/Rocket.Chat/pull/20408) by [@im-adithya](https://github.com/im-adithya)) @@ -14322,22 +14180,14 @@ - Several Slack Importer issues ([#20216](https://github.com/RocketChat/Rocket.Chat/pull/20216)) - - Fix: Slack Importer crashes when importing a large users.json file - - - Fix: Slack importer crashes when messages have invalid mentions - - - Skip listing all users on the preparation screen when the user count is too large. - - - Split avatar download into a separate process. - - - Update room's last message when the import is complete. - - - Prevent invalid or duplicated channel names - - - Improve message error handling. - - - Reduce max allowed BSON size to avoid possible issues in some servers. - + - Fix: Slack Importer crashes when importing a large users.json file + - Fix: Slack importer crashes when messages have invalid mentions + - Skip listing all users on the preparation screen when the user count is too large. + - Split avatar download into a separate process. + - Update room's last message when the import is complete. + - Prevent invalid or duplicated channel names + - Improve message error handling. + - Reduce max allowed BSON size to avoid possible issues in some servers. - Improve handling of very large channel files. - star icon was visible after unstarring a message ([#19645](https://github.com/RocketChat/Rocket.Chat/pull/19645) by [@bhavayAnand9](https://github.com/bhavayAnand9)) @@ -14356,15 +14206,15 @@ - User statuses in admin user info panel ([#20341](https://github.com/RocketChat/Rocket.Chat/pull/20341) by [@RonLek](https://github.com/RonLek)) - Modifies user statuses in admin info panel based on their actual status instead of their `statusConnection`. This enables correct and consistent change in user statuses. - Also, bot users having status as online were classified as offline, with this change they are now correctly classified based on their corresponding statuses. - + Modifies user statuses in admin info panel based on their actual status instead of their `statusConnection`. This enables correct and consistent change in user statuses. + Also, bot users having status as online were classified as offline, with this change they are now correctly classified based on their corresponding statuses. + https://user-images.githubusercontent.com/28918901/105624438-b8bcc500-5e47-11eb-8d1e-3a4180da1304.mp4 - Users autocomplete showing duplicated results ([#20481](https://github.com/RocketChat/Rocket.Chat/pull/20481) by [@Darshilp326](https://github.com/Darshilp326)) - Added new query for outside room users so that room members are not shown twice. - + Added new query for outside room users so that room members are not shown twice. + https://user-images.githubusercontent.com/55157259/106174582-33c10b00-61bb-11eb-9716-377ef7bba34e.mp4 <details> @@ -14385,7 +14235,7 @@ - Chore: Disable Sessions Aggregates tests locally ([#20607](https://github.com/RocketChat/Rocket.Chat/pull/20607)) - Disable Session aggregates tests in local environments + Disable Session aggregates tests in local environments For context, refer to: #20161 - Chore: Improve performance of messages’ watcher ([#20519](https://github.com/RocketChat/Rocket.Chat/pull/20519)) @@ -14620,20 +14470,18 @@ - **ENTERPRISE:** Omnichannel Contact Manager as preferred agent for routing ([#20244](https://github.com/RocketChat/Rocket.Chat/pull/20244)) - If the `Contact-Manager` is assigned to a Visitor, the chat will automatically get transferred to the respective Contact-Manager, provided the Contact-Manager is online. In-case the Contact-Manager is offline, the chat will be transferred to any other online agent. - We have provided a setting to control this auto-assignment feature -  - - Behavior based-on Routing method - - - 1. Auto-selection, Load-Balancing, or External Service (`autoAssignAgent = true`) - This is straightforward, - - if the Contact-manager is online, the chat will be transferred to the Contact-Manger only - - if the Contact-manager is offline, the chat will be transferred to any other online-agent based on the Routing system - - 2. Manual-selection (`autoAssignAgent = false`) - - If the Contact-Manager is online, the chat will appear in the Queue of Contact-Manager **ONLY** + If the `Contact-Manager` is assigned to a Visitor, the chat will automatically get transferred to the respective Contact-Manager, provided the Contact-Manager is online. In-case the Contact-Manager is offline, the chat will be transferred to any other online agent. + We have provided a setting to control this auto-assignment feature +  + + Behavior based-on Routing method + + 1. Auto-selection, Load-Balancing, or External Service (`autoAssignAgent = true`) + This is straightforward, + - if the Contact-manager is online, the chat will be transferred to the Contact-Manger only + - if the Contact-manager is offline, the chat will be transferred to any other online-agent based on the Routing system + 2. Manual-selection (`autoAssignAgent = false`) + - If the Contact-Manager is online, the chat will appear in the Queue of Contact-Manager **ONLY** - If the Contact-Manager is offline, the chat will appear in the Queue of all related Agents/Manager ( like it's done right now ) - Banner system and NPS ([#20221](https://github.com/RocketChat/Rocket.Chat/pull/20221)) @@ -14642,34 +14490,34 @@ - Email Inboxes for Omnichannel ([#20101](https://github.com/RocketChat/Rocket.Chat/pull/20101) by [@rafaelblink](https://github.com/rafaelblink)) - With this new feature, email accounts will receive email messages(threads) which will be transformed into Omnichannel chats. It'll be possible to set up multiple email accounts, test the connection with email server(email provider) and define the behaviour of each account. - - https://user-images.githubusercontent.com/2493803/105430398-242d4980-5c32-11eb-835a-450c94837d23.mp4 - - ### New item on admin menu - -  - - - ### Send test email tooltip - -  - - - ### Inbox Info - -  - - ### SMTP Info - -  - - ### IMAP Info - -  - - ### Messages - + With this new feature, email accounts will receive email messages(threads) which will be transformed into Omnichannel chats. It'll be possible to set up multiple email accounts, test the connection with email server(email provider) and define the behaviour of each account. + + https://user-images.githubusercontent.com/2493803/105430398-242d4980-5c32-11eb-835a-450c94837d23.mp4 + + ### New item on admin menu + +  + + + ### Send test email tooltip + +  + + + ### Inbox Info + +  + + ### SMTP Info + +  + + ### IMAP Info + +  + + ### Messages +  - Encrypted Discussions and new Encryption Permissions ([#20201](https://github.com/RocketChat/Rocket.Chat/pull/20201)) @@ -14681,7 +14529,7 @@ - Add extra SAML settings to update room subs and add private room subs. ([#19489](https://github.com/RocketChat/Rocket.Chat/pull/19489) by [@tlskinneriv](https://github.com/tlskinneriv)) - Added a SAML setting to support updating room subscriptions each time a user logs in via SAML. + Added a SAML setting to support updating room subscriptions each time a user logs in via SAML. Added a SAML setting to support including private rooms in SAML updated subscriptions (whether initial or on each logon). - Autofocus on directory ([#20509](https://github.com/RocketChat/Rocket.Chat/pull/20509)) @@ -14708,7 +14556,7 @@ - Tooltip added for Kebab menu on chat header ([#20116](https://github.com/RocketChat/Rocket.Chat/pull/20116)) - Added the missing Tooltip for kebab menu on chat header. + Added the missing Tooltip for kebab menu on chat header.  ### 🛠Bug fixes @@ -14730,7 +14578,7 @@ - Added context check for closing active tabbar for member-list ([#20228](https://github.com/RocketChat/Rocket.Chat/pull/20228)) - When we click on a username and then click on see user's full profile, a tab gets active and shows us the user's profile, the problem occurs when the tab is still active and we try to see another user's profile. In this case, tabbar gets closed. + When we click on a username and then click on see user's full profile, a tab gets active and shows us the user's profile, the problem occurs when the tab is still active and we try to see another user's profile. In this case, tabbar gets closed. To resolve this, added context check for closing action of active tabbar. - Added Margin between status bullet and status label ([#20199](https://github.com/RocketChat/Rocket.Chat/pull/20199)) @@ -14739,8 +14587,8 @@ - Added success message on saving notification preference. ([#20220](https://github.com/RocketChat/Rocket.Chat/pull/20220) by [@Darshilp326](https://github.com/Darshilp326)) - Added success message after saving notification preferences. - + Added success message after saving notification preferences. + https://user-images.githubusercontent.com/55157259/104774617-03ca3e80-579d-11eb-8fa4-990b108dd8d9.mp4 - Admin User Info email verified status ([#20110](https://github.com/RocketChat/Rocket.Chat/pull/20110) by [@bdelwood](https://github.com/bdelwood)) @@ -14749,10 +14597,10 @@ - Change header's favorite icon to filled star ([#20174](https://github.com/RocketChat/Rocket.Chat/pull/20174)) - ### Before: -  - - ### After: + ### Before: +  + + ### After:  - Changed success message for adding custom sound. ([#20272](https://github.com/RocketChat/Rocket.Chat/pull/20272) by [@Darshilp326](https://github.com/Darshilp326)) @@ -14761,24 +14609,24 @@ - Changed success message for ignoring member. ([#19996](https://github.com/RocketChat/Rocket.Chat/pull/19996) by [@Darshilp326](https://github.com/Darshilp326)) - Different messages for ignoring/unignoring will be displayed. - + Different messages for ignoring/unignoring will be displayed. + https://user-images.githubusercontent.com/55157259/103310307-4241c880-4a3d-11eb-8c6c-4c9b99d023db.mp4 - Creation of Omnichannel rooms not working correctly through the Apps when the agent parameter is set ([#19997](https://github.com/RocketChat/Rocket.Chat/pull/19997)) - Engagement dashboard graphs labels superposing each other ([#20267](https://github.com/RocketChat/Rocket.Chat/pull/20267)) - Now after a certain breakpoint, the graphs should stack vertically, and overlapping text rotated. - + Now after a certain breakpoint, the graphs should stack vertically, and overlapping text rotated. +  - Fields overflowing page ([#20287](https://github.com/RocketChat/Rocket.Chat/pull/20287)) - ### Before -  - - ### After + ### Before +  + + ### After  - Fix error that occurs on changing archive status of room ([#20098](https://github.com/RocketChat/Rocket.Chat/pull/20098) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -14795,7 +14643,7 @@ - Livechat.RegisterGuest method removing unset fields ([#20124](https://github.com/RocketChat/Rocket.Chat/pull/20124) by [@rafaelblink](https://github.com/rafaelblink)) - After changes made on https://github.com/RocketChat/Rocket.Chat/pull/19931, the `Livechat.RegisterGuest` method started removing properties from the visitor inappropriately. The properties that did not receive value were removed from the object. + After changes made on https://github.com/RocketChat/Rocket.Chat/pull/19931, the `Livechat.RegisterGuest` method started removing properties from the visitor inappropriately. The properties that did not receive value were removed from the object. Those changes were made to support the new Contact Form, but now the form has its own method to deal with Contact data so those changes are no longer necessary. - Markdown added for Header Room topic ([#20021](https://github.com/RocketChat/Rocket.Chat/pull/20021)) @@ -14816,18 +14664,18 @@ - Omnichannel - Contact Center form is not validating custom fields properly ([#20196](https://github.com/RocketChat/Rocket.Chat/pull/20196) by [@rafaelblink](https://github.com/rafaelblink)) - The contact form is accepting undefined values in required custom fields when creating or editing contacts, and, the errror message isn't following Rocket.chat design system. - - ### Before -  - - ### After - - #### New -  - - - #### Edit + The contact form is accepting undefined values in required custom fields when creating or editing contacts, and, the errror message isn't following Rocket.chat design system. + + ### Before +  + + ### After + + #### New +  + + + #### Edit  - Omnichannel Agents unable to take new chats in the queue ([#20022](https://github.com/RocketChat/Rocket.Chat/pull/20022) by [@rafaelblink](https://github.com/rafaelblink)) @@ -14848,15 +14696,15 @@ - Room special name in prompts ([#20277](https://github.com/RocketChat/Rocket.Chat/pull/20277) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - The "Hide room" and "Leave Room" confirmation prompts use the "name" key from the room info. When the setting " - Allow Special Characters in Room Names" is enabled, the prompts show the normalized names instead of those that contain the special characters. - - Changed the value being used from name to fname, which always has the user-set name. - - Previous: -  - - Updated: + The "Hide room" and "Leave Room" confirmation prompts use the "name" key from the room info. When the setting " + Allow Special Characters in Room Names" is enabled, the prompts show the normalized names instead of those that contain the special characters. + + Changed the value being used from name to fname, which always has the user-set name. + + Previous: +  + + Updated:  - Room's list showing all rooms with same name ([#20176](https://github.com/RocketChat/Rocket.Chat/pull/20176)) @@ -14867,9 +14715,9 @@ - Saving with blank email in edit user ([#20259](https://github.com/RocketChat/Rocket.Chat/pull/20259) by [@RonLek](https://github.com/RonLek)) - Disallows showing a success popup when email field is made blank in Edit User and instead shows the relevant error popup. - - + Disallows showing a success popup when email field is made blank in Edit User and instead shows the relevant error popup. + + https://user-images.githubusercontent.com/28918901/104960749-dbd81680-59fa-11eb-9c7b-2b257936f894.mp4 - Search list filter ([#19937](https://github.com/RocketChat/Rocket.Chat/pull/19937)) @@ -14916,7 +14764,7 @@ - Add translation of Edit Status in all languages ([#19916](https://github.com/RocketChat/Rocket.Chat/pull/19916) by [@sushant52](https://github.com/sushant52)) - Closes [#19915](https://github.com/RocketChat/Rocket.Chat/issues/19915) + Closes [#19915](https://github.com/RocketChat/Rocket.Chat/issues/19915) The profile options menu is well translated in many languages. However, Edit Status is the only button which is not well translated. With this change, the whole profile options will be properly translated in a lot of languages. - Bump axios from 0.18.0 to 0.18.1 ([#20055](https://github.com/RocketChat/Rocket.Chat/pull/20055) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -14951,10 +14799,10 @@ - Regression: Announcement bar not showing properly Markdown content ([#20290](https://github.com/RocketChat/Rocket.Chat/pull/20290)) - **Before**: -  - - **After**: + **Before**: +  + + **After**:  - regression: Announcement link open in new tab ([#20435](https://github.com/RocketChat/Rocket.Chat/pull/20435)) @@ -14969,23 +14817,23 @@ - Regression: Change sort icon ([#20177](https://github.com/RocketChat/Rocket.Chat/pull/20177)) - ### Before -  - - ### After + ### Before +  + + ### After  - Regression: Custom field labels are not displayed properly on Omnichannel Contact Profile form ([#20393](https://github.com/RocketChat/Rocket.Chat/pull/20393) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before -  - -  - - ### After - -  - + ### Before +  + +  + + ### After + +  +  - Regression: ESLint Warning - explicit-function-return-type ([#20434](https://github.com/RocketChat/Rocket.Chat/pull/20434) by [@aditya-mitra](https://github.com/aditya-mitra)) @@ -15002,8 +14850,8 @@ - Regression: Fixed update room avatar issue. ([#20433](https://github.com/RocketChat/Rocket.Chat/pull/20433) by [@Darshilp326](https://github.com/Darshilp326)) - Users can now update their room avatar without any error. - + Users can now update their room avatar without any error. + https://user-images.githubusercontent.com/55157259/105951602-560d3880-6096-11eb-97a5-b5eb9a28b58d.mp4 - Regression: Info Page Icon style and usage graph breaking ([#20180](https://github.com/RocketChat/Rocket.Chat/pull/20180)) @@ -15020,11 +14868,11 @@ - Regression: Unread superposing announcement. ([#20306](https://github.com/RocketChat/Rocket.Chat/pull/20306)) - ### Before -  - - - ### After + ### Before +  + + + ### After  - Regression: User Dropdown margin ([#20222](https://github.com/RocketChat/Rocket.Chat/pull/20222)) @@ -15312,8 +15160,8 @@ - Hightlights validation on Account Preferences page ([#19902](https://github.com/RocketChat/Rocket.Chat/pull/19902) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - This PR fixes two issues in the account settings "preferences" panel. - Once set, the "Highlighted Words" setting cannot be reset to an empty string. This was fixed by changing the string validation from checking the length to checking the type of variable. + This PR fixes two issues in the account settings "preferences" panel. + Once set, the "Highlighted Words" setting cannot be reset to an empty string. This was fixed by changing the string validation from checking the length to checking the type of variable. Secondly, it tracks the changes to correctly identify if changes after the last "save changes" action have been made, using an "updates" state variable, instead of just comparing against the initialValue that does not change on clicking "save changes". - Image preview for image URLs on messages ([#19734](https://github.com/RocketChat/Rocket.Chat/pull/19734) by [@g-thome](https://github.com/g-thome)) @@ -15372,14 +15220,10 @@ - Chore: Update Pull Request template ([#19768](https://github.com/RocketChat/Rocket.Chat/pull/19768)) - Improve the template of Pull Requests in order to make it clear reducing duplicated information and removing the visible checklists that were generating noise and misunderstanding with the PR progress. - - - Moved the checklists to inside comments - - - Merge the changelog and proposed changes sections to have a single source of description that goes to the changelog - - - Remove the screenshot section, they can be added inside the description - + Improve the template of Pull Requests in order to make it clear reducing duplicated information and removing the visible checklists that were generating noise and misunderstanding with the PR progress. + - Moved the checklists to inside comments + - Merge the changelog and proposed changes sections to have a single source of description that goes to the changelog + - Remove the screenshot section, they can be added inside the description - Changed the proposed changes title to incentivizing the usage of images and videos - Frontend folder structure ([#19631](https://github.com/RocketChat/Rocket.Chat/pull/19631)) @@ -15414,11 +15258,11 @@ - Regression: Double Scrollbars on tables ([#19980](https://github.com/RocketChat/Rocket.Chat/pull/19980)) - Before: -  - - - After: + Before: +  + + + After:  - Regression: Failed autolinker and markdown rendering ([#19831](https://github.com/RocketChat/Rocket.Chat/pull/19831)) @@ -15437,7 +15281,7 @@ - Regression: Omnichannel Custom Fields Form no longer working after refactoring ([#19948](https://github.com/RocketChat/Rocket.Chat/pull/19948)) - The Omnichannel `Custom Fields` form is not working anymore after some refactorings on client-side. + The Omnichannel `Custom Fields` form is not working anymore after some refactorings on client-side. When the user clicks on `Custom Field` in the Omnichannel menu, a blank page appears. - Regression: polishing licenses endpoints ([#19981](https://github.com/RocketChat/Rocket.Chat/pull/19981) by [@g-thome](https://github.com/g-thome)) @@ -15636,8 +15480,8 @@ - Bundle Size Client ([#19533](https://github.com/RocketChat/Rocket.Chat/pull/19533)) - temporarily removes some codeblock languages - Moved some libraries to dynamic imports + temporarily removes some codeblock languages + Moved some libraries to dynamic imports Removed some shared code not used on the client side - Forward Omnichannel room to agent in another department ([#19576](https://github.com/RocketChat/Rocket.Chat/pull/19576) by [@mrfigueiredo](https://github.com/mrfigueiredo)) @@ -16718,10 +16562,8 @@ - **2FA:** Password enforcement setting and 2FA protection when saving settings or resetting E2E encryption ([#18640](https://github.com/RocketChat/Rocket.Chat/pull/18640)) - - Increase the 2FA remembering time from 5min to 30min - - - Add new setting to enforce 2FA password fallback (enabled only for new installations) - + - Increase the 2FA remembering time from 5min to 30min + - Add new setting to enforce 2FA password fallback (enabled only for new installations) - Require 2FA to save settings and reset E2E Encryption keys - **Omnichannel:** Allow set other agent status via method `livechat:changeLivechatStatus ` ([#18571](https://github.com/RocketChat/Rocket.Chat/pull/18571)) @@ -16739,7 +16581,7 @@ - 2FA by Email setting showing for the user even when disabled by the admin ([#18473](https://github.com/RocketChat/Rocket.Chat/pull/18473)) - The option to disable/enable the **Two-factor authentication via Email** at `Account > Security > Two Factor Authentication + The option to disable/enable the **Two-factor authentication via Email** at `Account > Security > Two Factor Authentication ` was visible even when the setting **Enable Two Factor Authentication via Email** at `Admin > Accounts > Two Factor Authentication` was disabled leading to misbehavior since the functionality was disabled. - Agents enabledDepartment attribute not set on collection ([#18614](https://github.com/RocketChat/Rocket.Chat/pull/18614) by [@paulobernardoaf](https://github.com/paulobernardoaf)) @@ -17089,16 +16931,13 @@ - Mention autocomplete UI and performance improvements ([#18309](https://github.com/RocketChat/Rocket.Chat/pull/18309)) - * New setting to configure the number of suggestions `Admin > Layout > User Interface > Number of users' autocomplete suggestions` (default 5) - - * The UI shows whenever the user is not a member of the room - - * The UI shows when the suggestion came from the last messages for quick selection/reply - - * The suggestions follow this order: - * The user with the exact username and member of the room - * The user with the exact username but not a member of the room (if allowed to list non-members) - * The users containing the text in username, name or nickname and member of the room + * New setting to configure the number of suggestions `Admin > Layout > User Interface > Number of users' autocomplete suggestions` (default 5) + * The UI shows whenever the user is not a member of the room + * The UI shows when the suggestion came from the last messages for quick selection/reply + * The suggestions follow this order: + * The user with the exact username and member of the room + * The user with the exact username but not a member of the room (if allowed to list non-members) + * The users containing the text in username, name or nickname and member of the room * The users containing the text in username, name or nickname and not a member of the room (if allowed to list non-members) - Message action styles ([#18190](https://github.com/RocketChat/Rocket.Chat/pull/18190)) @@ -17440,10 +17279,10 @@ - Split NOTIFICATIONS_SCHEDULE_DELAY into three separate variables ([#17669](https://github.com/RocketChat/Rocket.Chat/pull/17669) by [@jazztickets](https://github.com/jazztickets)) - Email notification delay can now be customized with the following environment variables: - NOTIFICATIONS_SCHEDULE_DELAY_ONLINE - NOTIFICATIONS_SCHEDULE_DELAY_AWAY - NOTIFICATIONS_SCHEDULE_DELAY_OFFLINE + Email notification delay can now be customized with the following environment variables: + NOTIFICATIONS_SCHEDULE_DELAY_ONLINE + NOTIFICATIONS_SCHEDULE_DELAY_AWAY + NOTIFICATIONS_SCHEDULE_DELAY_OFFLINE Setting the value to -1 disable notifications for that type. - Threads ([#17416](https://github.com/RocketChat/Rocket.Chat/pull/17416)) @@ -17843,11 +17682,11 @@ - **ENTERPRISE:** Omnichannel Last-Chatted Agent Preferred option ([#17666](https://github.com/RocketChat/Rocket.Chat/pull/17666)) - If activated, this feature will store the last agent that assisted each Omnichannel visitor when a conversation is taken. So, when a visitor returns(it works with any entry point, Livechat, Facebook, REST API, and so on) and starts a new chat, the routing system checks: - - 1 - The visitor object for any stored agent that the visitor has previously talked to; - 2 - If a previous agent is not found, the system will try to find a previous conversation of the same visitor. If a room is found, the system will get the previous agent from the room; - + If activated, this feature will store the last agent that assisted each Omnichannel visitor when a conversation is taken. So, when a visitor returns(it works with any entry point, Livechat, Facebook, REST API, and so on) and starts a new chat, the routing system checks: + + 1 - The visitor object for any stored agent that the visitor has previously talked to; + 2 - If a previous agent is not found, the system will try to find a previous conversation of the same visitor. If a room is found, the system will get the previous agent from the room; + After this process, if an agent has been found, the system will check the agent's availability to assist the new chat. If it's not available, then the routing system will get the next available agent in the queue. - **ENTERPRISE:** Support for custom Livechat registration form fields ([#17581](https://github.com/RocketChat/Rocket.Chat/pull/17581)) @@ -17952,12 +17791,9 @@ - Notification sounds ([#17616](https://github.com/RocketChat/Rocket.Chat/pull/17616)) - * Global CDN config was ignored when loading the sound files - - * Upload of custom sounds wasn't getting the file extension correctly - - * Some translations were missing - + * Global CDN config was ignored when loading the sound files + * Upload of custom sounds wasn't getting the file extension correctly + * Some translations were missing * Edit and delete of custom sounds were not working correctly - Omnichannel departments are not saved when the offline channel name is not defined ([#17553](https://github.com/RocketChat/Rocket.Chat/pull/17553)) @@ -18245,19 +18081,14 @@ - Better Push and Email Notification logic ([#17357](https://github.com/RocketChat/Rocket.Chat/pull/17357)) - We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules: - - - - When the user is online the notification is scheduled to be sent in 120 seconds - - - When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away - - - When the user is offline the notification is scheduled to be sent right away - - - When the user reads a channel all the notifications for that user are removed (clear queue) - - - When a notification is processed to be sent to a user and there are other scheduled notifications: - - All the scheduled notifications for that user are rescheduled to now + We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules: + + - When the user is online the notification is scheduled to be sent in 120 seconds + - When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away + - When the user is offline the notification is scheduled to be sent right away + - When the user reads a channel all the notifications for that user are removed (clear queue) + - When a notification is processed to be sent to a user and there are other scheduled notifications: + - All the scheduled notifications for that user are rescheduled to now - The current notification goes back to the queue to be processed ordered by creation date - Buttons to check/uncheck all users and channels on import ([#17207](https://github.com/RocketChat/Rocket.Chat/pull/17207)) @@ -18620,7 +18451,7 @@ - Translation via MS translate ([#16363](https://github.com/RocketChat/Rocket.Chat/pull/16363) by [@mrsimpson](https://github.com/mrsimpson)) - Adds Microsoft's translation service (https://translator.microsoft.com/) as a provider for translation of messages. + Adds Microsoft's translation service (https://translator.microsoft.com/) as a provider for translation of messages. In addition to implementing the interface (similar to google and DeepL), a small change has been done in order to display the translation provider on the UI. - Two Factor authentication via email ([#15949](https://github.com/RocketChat/Rocket.Chat/pull/15949)) diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 2a403759721..f667f4c5465 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-develop +ENV RC_VERSION 5.2.0-rc.0 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index b70e9be373d..75487158bb6 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-develop" + "version": "5.2.0-rc.0" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cd26881d8dc..1087a5bb473 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-develop", + "version": "5.2.0-rc.0", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 86462fe179f..8b78f3fbcd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-develop", + "version": "5.2.0-rc.0", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From ca43434f44862c30e813efa01b8d69541d3e13f6 Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:11:57 +0000 Subject: [PATCH 076/107] =?UTF-8?q?i18n:=20Language=20update=20from=20Ling?= =?UTF-8?q?oHub=20=F0=9F=A4=96=20on=202022-09-26Z=20(#26948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .../rocketchat-i18n/i18n/af.i18n.json | 1 - .../rocketchat-i18n/i18n/ar.i18n.json | 5 ++- .../rocketchat-i18n/i18n/az.i18n.json | 1 - .../rocketchat-i18n/i18n/be-BY.i18n.json | 3 +- .../rocketchat-i18n/i18n/bg.i18n.json | 1 - .../rocketchat-i18n/i18n/bs.i18n.json | 1 - .../rocketchat-i18n/i18n/ca.i18n.json | 5 ++- .../rocketchat-i18n/i18n/cs.i18n.json | 1 - .../rocketchat-i18n/i18n/cy.i18n.json | 1 - .../rocketchat-i18n/i18n/da.i18n.json | 1 - .../rocketchat-i18n/i18n/de-AT.i18n.json | 1 - .../rocketchat-i18n/i18n/de.i18n.json | 34 +++++++++--------- .../rocketchat-i18n/i18n/el.i18n.json | 1 - .../rocketchat-i18n/i18n/en.i18n.json | 6 +++- .../rocketchat-i18n/i18n/eo.i18n.json | 1 - .../rocketchat-i18n/i18n/es.i18n.json | 5 ++- .../rocketchat-i18n/i18n/fa.i18n.json | 1 - .../rocketchat-i18n/i18n/fi.i18n.json | 1 - .../rocketchat-i18n/i18n/fr.i18n.json | 5 ++- .../rocketchat-i18n/i18n/hr.i18n.json | 1 - .../rocketchat-i18n/i18n/hu.i18n.json | 3 +- .../rocketchat-i18n/i18n/id.i18n.json | 1 - .../rocketchat-i18n/i18n/it.i18n.json | 3 +- .../rocketchat-i18n/i18n/ja.i18n.json | 5 ++- .../rocketchat-i18n/i18n/km.i18n.json | 1 - .../rocketchat-i18n/i18n/ko.i18n.json | 1 - .../rocketchat-i18n/i18n/ku.i18n.json | 1 - .../rocketchat-i18n/i18n/lo.i18n.json | 1 - .../rocketchat-i18n/i18n/lt.i18n.json | 1 - .../rocketchat-i18n/i18n/lv.i18n.json | 1 - .../rocketchat-i18n/i18n/mn.i18n.json | 1 - .../rocketchat-i18n/i18n/ms-MY.i18n.json | 1 - .../rocketchat-i18n/i18n/nl.i18n.json | 5 ++- .../rocketchat-i18n/i18n/no.i18n.json | 1 - .../rocketchat-i18n/i18n/pl.i18n.json | 36 +++++++++---------- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 8 ++--- .../rocketchat-i18n/i18n/pt.i18n.json | 1 - .../rocketchat-i18n/i18n/ro.i18n.json | 1 - .../rocketchat-i18n/i18n/ru.i18n.json | 7 ++-- .../rocketchat-i18n/i18n/sk-SK.i18n.json | 1 - .../rocketchat-i18n/i18n/sl-SI.i18n.json | 1 - .../rocketchat-i18n/i18n/sq.i18n.json | 1 - .../rocketchat-i18n/i18n/sr.i18n.json | 1 - .../rocketchat-i18n/i18n/sv.i18n.json | 1 - .../rocketchat-i18n/i18n/ta-IN.i18n.json | 1 - .../rocketchat-i18n/i18n/th-TH.i18n.json | 1 - .../rocketchat-i18n/i18n/tr.i18n.json | 1 - .../rocketchat-i18n/i18n/uk.i18n.json | 1 - .../rocketchat-i18n/i18n/vi-VN.i18n.json | 1 - .../rocketchat-i18n/i18n/zh-HK.i18n.json | 1 - .../rocketchat-i18n/i18n/zh-TW.i18n.json | 5 ++- .../rocketchat-i18n/i18n/zh.i18n.json | 1 - 52 files changed, 63 insertions(+), 109 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json index 67d52f6003b..7f100c9d5bd 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/af.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "Lêer opgelaai", "files": "lêers", "Files_only": "Verwyder slegs die aangehegte lêers, hou boodskappe", - "files_pruned": "lêers gesny", "FileUpload": "Lêeroplaai", "FileUpload_Disabled": "Lêeroplaaie is gedeaktiveer.", "FileUpload_Enabled": "Lêeroplaaie geaktiveer", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index d6fe2e12993..bc900a90f63 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "سيتم تعيين مالك جديد تلقائيًا إلى <span style=\"font-weight: bold;\">__count__</span> من الغرÙ.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "سيتم تعيين مالك جديد تلقائيًا إلى الغرÙØ© <span style=\"font-weight: bold;\"> __roomName__ </span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "سيتم تعيين مالك جديد تلقائيًا إلى هذه الغر٠البالغ عددها <span style=\"font-weight: bold;\">__count__</span> :<br/> __rooms__.", - "Accept": "قبول", "Accept_Call": "قبول المكالمة", + "Accept": "قبول", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "قبول طلبات الدردشة الواردة عبر القناة متعددة الاتجاهات وإن لم يكن هناك وكلاء على الإنترنت", "Accept_new_livechats_when_agent_is_idle": "قبول طلبات الدردشة الجديدة عبر القناة متعددة الاتجاهات عندما يكون الوكيل معطلاً", "Accept_with_no_online_agents": "القبول Øال عدم وجدود وكلاء على الإنترنت", @@ -1975,7 +1975,6 @@ "files": "الملÙات", "Files": "الملÙات", "Files_only": "إزالة الملÙات المرÙقة Ùقط، والØÙاظ على الرسائل", - "files_pruned": "الملÙات المنقØØ©", "FileSize_Bytes": "__fileSize__ بايت", "FileSize_KB": "__fileSize__ كيلوبايت", "FileSize_MB": "__fileSize__ ميجابايت", @@ -2116,6 +2115,7 @@ "Global_purge_override_warning": "توجد سياسة عامة للاØتÙاظ. إذا تركت \"تجاوز نهج الاستبقاء العام\" قيد إيقا٠التشغيل، Ùيمكنك Ùقط تطبيق سياسة أكثر صرامة من السياسة العامة.", "Global_Search": "بØØ« عام", "Go_to_your_workspace": "الانتقال إلى مساØØ© العمل الخاصة بك", + "Hold_Call": "إخÙاء المكالمة", "GoogleCloudStorage": "التخزين السØابي على Google", "GoogleNaturalLanguage_ServiceAccount_Description": "مل٠JSON لمÙØªØ§Ø Øساب الخدمة. يمكن العثور على مزيد من المعلومات [هنا](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "معر٠مدير علامات Google", @@ -2160,7 +2160,6 @@ "Highlights_List": "تظليل الكلمات", "History": "المØÙوظات", "Hold_Time": "إخÙاء الاسم", - "Hold_Call": "إخÙاء المكالمة", "Home": "الصÙØØ© الرئيسية", "Host": "مضيÙ", "Hospitality_Businness": "أعمال الضياÙØ©", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json index 9e7da93b87e..dbe78d0b5ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/az.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "Fayl yüklÉ™ndi", "files": "faylları", "Files_only": "Yalnız É™lavÉ™ faylları silin, mesajı saxlayın", - "files_pruned": "fayllar kÉ™silmiÅŸdir", "FileUpload": "Fayl yüklÉ™", "FileUpload_Disabled": "Fayl yüklÉ™mÉ™lÉ™ri aradan qaldırıldı.", "FileUpload_Enabled": "Fayl yüklÉ™mÉ™lÉ™ri aktivləşmiÅŸdir", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json index 9a5deee6844..4c3690a60c3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/be-BY.i18n.json @@ -11,8 +11,8 @@ "2_Erros_Information_and_Debug": "2 - Памылкі, Ñ–Ð½Ñ„Ð°Ñ€Ð¼Ð°Ñ†Ñ‹Ñ Ñ– адладка", "12_Hour": "12-гадзінавы фармат", "24_Hour": "24-гадзінавы фармат", - "Accept": "ПрынÑць", "Accept_Call": "ПрынÑць выклік", + "Accept": "ПрынÑць", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Прымаць ÑƒÐ²Ð°Ñ…Ð¾Ð´Ð½Ñ‹Ñ Ð·Ð°Ð¿Ñ‹Ñ‚Ñ‹ Livechat, нават калі нÑма онлайн агентаў", "Accept_with_no_online_agents": "Прымаць з неподключеннымі Ñупрацоўнікамі", "Access_not_authorized": "ДоÑтуп не аўтарызаваны", @@ -1158,7 +1158,6 @@ "File_uploaded": "файл загружаны", "files": "файлы", "Files_only": "выдаліць толькі ÑƒÐºÐ»Ð°Ð´Ð·ÐµÐ½Ñ‹Ñ Ñ„Ð°Ð¹Ð»Ñ‹, захоўваць паведамленні", - "files_pruned": "файлы абразанні", "FileUpload": "Ðазвы пад аховай", "FileUpload_Disabled": "Ўкладанні файлаў.", "FileUpload_Enabled": "Загружаных файлаў Enabled", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json index 5d968b448e3..d870001426b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bg.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "Файлът е качен", "files": "файлове", "Files_only": "ОтÑтранÑвайте Ñамо приложените файлове, запазвайте ÑъобщениÑта", - "files_pruned": "файлове отрÑзани", "FileUpload": "Качване на файл", "FileUpload_Disabled": "Качването на файлове е деактивирано.", "FileUpload_Enabled": "Файловите ÐºÐ°Ñ‡Ð²Ð°Ð½Ð¸Ñ Ñа активирани", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json index 1f9a2979297..736f56b0a37 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/bs.i18n.json @@ -1137,7 +1137,6 @@ "File_uploaded": "Datoteka je prenesena", "files": "slika", "Files_only": "Samo uklonite priložene datoteke, zadržite poruke", - "files_pruned": "datoteke su obrezane", "FileUpload": "Prijenos datoteke", "FileUpload_Disabled": "Prijenosi datoteka su onemogućeni", "FileUpload_Enabled": "Prijenos Datoteka Omogućeno", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index e939943f8b6..0fc91187be9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nou propietari serà assignat automà ticament a les <span style=\"font-weight: bold;\">__count__</span> sales.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nou propietari s'assignarà automà ticament a la sala <span style=\"font-weight: bold;\">__roomName__</span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "S'assignarà automà ticament un nou propietari a les <span style=\"font-weight: bold;\">__count__</span> sales: <br/> __rooms__.", - "Accept": "Accepta", "Accept_Call": "Accepta Trucada", + "Accept": "Accepta", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Acceptar sol·licituds entrants de LiveChat encara que no hagi agents en lÃnia", "Accept_new_livechats_when_agent_is_idle": "Accepteu les noves sol·licituds LiveChat quan l'agent està inactiu", "Accept_with_no_online_agents": "Acceptar sense agents en lÃnia", @@ -1954,7 +1954,6 @@ "files": "fitxers", "Files": "Arxius", "Files_only": "Només elimini els arxius adjunts, mantingui missatges", - "files_pruned": "arxius esborrats", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2093,6 +2092,7 @@ "Global_purge_override_warning": "Hi ha una polÃtica de retenció global. Si deixa desactivada l'opció \"Anul·lar la polÃtica de retenció global\", només pot aplicar una polÃtica que sigui més estricta que la polÃtica global.", "Global_Search": "Cerca global", "Go_to_your_workspace": "Aneu a l'espai de treball", + "Hold_Call": "Trucada en espera", "GoogleCloudStorage": "Emmagatzematge al núvol de Google", "GoogleNaturalLanguage_ServiceAccount_Description": "Arxiu JSON amb la clau del compte de servei (\"Service account key\"). Pots trobar més informació [aquÃ](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "ID de Google Tag Manager", @@ -2137,7 +2137,6 @@ "Highlights_List": "Ressalta paraules", "History": "Historial", "Hold_Time": "Temps d'espera", - "Hold_Call": "Trucada en espera", "Home": "Inici", "Host": "Amfitrió (host)", "Hospitality_Businness": "Negoci d'Hosteleria", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index e058d94d916..e1ce9f5dbfc 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -1640,7 +1640,6 @@ "files": "soubory", "Files": "Soubory", "Files_only": "Odstranit pouze pÅ™ipojené soubory, zachovat zprávy", - "files_pruned": "soubory proÄiÅ¡tÄ›ny", "FileSize_Bytes": "__fileSize__ Bytů", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json index f5f0b429938..a0fbe2b267d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cy.i18n.json @@ -1138,7 +1138,6 @@ "File_uploaded": "Ffeil wedi'i lwytho i fyny", "files": "ffeiliau", "Files_only": "Dim ond tynnu'r ffeiliau atodedig, cadw negeseuon", - "files_pruned": "ffeiliau wedi'u prunedu", "FileUpload": "Llwytho Ffeil", "FileUpload_Disabled": "Mae llwythiadau ffeil yn anabl.", "FileUpload_Enabled": "Llwythiadau Ffeil Hapus", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 22301c87a4a..4f7ed0c5cb7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -1650,7 +1650,6 @@ "files": "filer", "Files": "Filer", "Files_only": "Fjern kun de vedhæftede filer. Behold meddelelser", - "files_pruned": "filer beskÃ¥ret", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 96ffd4c81b1..fa3266dfe13 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -1144,7 +1144,6 @@ "File_uploaded": "Datei hochgeladen", "files": "Dateien", "Files_only": "Entferne nur die angehängten Dateien, behalte Nachrichten", - "files_pruned": "Dateien beschnitten", "FileUpload": "Dateien hochladen", "FileUpload_Disabled": "Datei-Uploads sind deaktiviert.", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 16d31389924..103ee4105b4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -23,13 +23,14 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Ein neuer Eigentümer wird automatisch <span style=\"font-weight: bold;\">__count__</span> Räumen zugewiesen.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Dem Raum <span style=\"font-weight: bold;\">__roomName__</span> wird automatisch ein neuer Eigentümer zugewiesen.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Folgenden <span style=\"font-weight: bold;\">__count__</span> Räumen wird automatisch ein neuer Eigentümer zugewiesen: <br/> __rooms__", - "Accept": "Akzeptieren", "Accept_Call": "Anruf annehmen", + "Accept": "Akzeptieren", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Akzeptiere eingehende Omnichannel-Anfragen auch wenn kein Agent online ist", "Accept_new_livechats_when_agent_is_idle": "Akzeptiere neue Omnichannel-Anfragen, wenn der Agent inaktiv ist", "Accept_with_no_online_agents": "Auch annehmen, wenn keine Agenten online sind", "Access_not_authorized": "Der Zugriff ist nicht gestattet.", "Access_Token_URL": "URL des Access-Token", + "Access_Your_Account": "Zugang zu Ihrem Konto", "access-mailer": "Zugriff auf den Mailer", "access-mailer_description": "Berechtigung, Massen-E-Mails an alle Benutzer zu versenden.", "access-permissions": "Zugriff auf die Berechtigungs-Ãœbersicht", @@ -1510,6 +1511,13 @@ "Details": "Details", "Device_Changes_Not_Available": "Gerätewechsel in diesem Browser nicht verfügbar. Für garantierte Verfügbarkeit verwenden Sie bitte die offizielle Desktop-App von Rocket.Chat.", "Device_Management": "Geräteverwaltung", + "Device_Management_Client": "Client", + "Device_Management_Device": "Gerät", + "Device_Management_Device_Unknown": "Unbekannt", + "Device_Management_Email_Subject": "[Site_Name] - Anmeldung erkannt", + "Device_Management_Email_Body": "Sie können die folgenden Platzhalter verwenden: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", + "Device_Management_IP": "IP", + "Device_Management_OS": "OS", "Device_ID": "Geräte-ID", "Device_Info": "Geräte-Infos", "Device_Logged_Out": "Gerät abgemeldet", @@ -2059,6 +2067,7 @@ "FEDERATION_Unique_Id_Description": "Dies ist Ihre eindeutige Verbund-ID, die zur Identifizierung Ihres Peers im Netz verwendet wird.", "Federation_Matrix": "Verbund V2", "Federation_Matrix_enabled": "Aktiviert", + "FileUpload_Disabled_for_federation": "Datei-Uploads sind für Verbundräume deaktiviert.", "Federation_Matrix_Enabled_Alert": "<a target=\"_blank\" href=\"https://go.rocket.chat/i/matrix-federation\">Weitere Informationen zur Unterstützung von Matrix Verbund finden Sie hier</> (Nach jeder Konfiguration ist ein Neustart erforderlich, damit die Änderungen wirksam werden)", "Federation_Matrix_Federated": "Verbunden", "Federation_Matrix_Federated_Description": "Wenn Sie einen Verbundraum erstellen, können Sie weder Verschlüsselung noch Broadcasting aktivieren", @@ -2102,7 +2111,6 @@ "FileUpload_Description": "Konfigurieren Sie das Hochladen und Speichern von Dateien.", "FileUpload_Cannot_preview_file": "Vorschau der Datei nicht möglich", "FileUpload_Disabled": "Datei-Uploads sind deaktiviert.", - "FileUpload_Disabled_for_federation": "Datei-Uploads sind für Verbundräume deaktiviert.", "FileUpload_Enable_json_web_token_for_files": "Json Web Tokens-Schutz zum Hochladen von Dateien aktivieren", "FileUpload_Enable_json_web_token_for_files_description": "Hängt eine JWT an die URLs der hochgeladenen Dateien an", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", @@ -2239,6 +2247,8 @@ "Global_purge_override_warning": "Eine globale Aufbewahrungsrichtlinie ist vorhanden. Wenn Sie die globale Aufbewahrungsrichtlinie außer Kraft setzen, können Sie nur eine Richtlinie anwenden, die strenger ist als die globale Richtlinie.", "Global_Search": "Globale Suche", "Go_to_your_workspace": "Auf zu Ihrem Arbeitsbereich!", + "Hold_Call": "Gespräch halten", + "Hold_Call_EE_only": "Anruf halten (nur Enterprise Edition)", "GoogleCloudStorage": "Google Cloud-Speicher", "GoogleNaturalLanguage_ServiceAccount_Description": "Service-Konto SchlüsselDatei (JSON). Weiterführende Informationen dazu [hier](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Google-Tag-Manager-ID", @@ -2285,8 +2295,6 @@ "Highlights_List": "Wörter hervorheben", "History": "Chronik", "Hold_Time": "Haltezeit", - "Hold_Call": "Gespräch halten", - "Hold_Call_EE_only": "Anruf halten (nur Enterprise Edition)", "Home": "Startseite", "Host": "Host", "Hospitality_Businness": "Gastronomie", @@ -2924,6 +2932,8 @@ "Logged_out_of_other_clients_successfully": "Sie wurden erfolgreich von anderen Geräten abgemeldet", "Login": "Anmelden", "Login_Attempts": "Fehlgeschlagene Anmeldeversuche", + "Login_Detected": "Anmeldung erkannt", + "Logged_In_Via": "Eingeloggt über", "Login_Logs": "Anmeldeprotokolle", "Login_Logs_ClientIp": "Client-IP bei fehlgeschlagenen Anmeldeversuchsprotokollen anzeigen", "Login_Logs_Enabled": "Protokollieren fehlgeschlagener Anmeldeversuche (auf der Konsole) ", @@ -3483,6 +3493,7 @@ "optional": "optional", "Options": "Optionen", "or": "oder", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Oder kopieren Sie diese URL und fügen Sie sie in eine Registerkarte Ihres Browsers ein", "Or_talk_as_anonymous": "Oder schreiben Sie anonym", "Order": "Auftrag", "Organization_Email": "Organisations-E-Mail", @@ -3555,6 +3566,7 @@ "Phone_call": "Telefonanruf", "Phone_Number": "Telefonnummer", "Thank_you_exclamation_mark": "Vielen Dank!", + "Thank_You_For_Choosing_RocketChat": "Danke, dass Sie sich für Rocket.Chat entschieden haben!", "Phone_already_exists": "Telefon ist bereits vorhanden", "Phone_number": "Telefonnummer", "PID": "PID", @@ -5246,17 +5258,5 @@ "onboarding.form.standaloneServerForm.title": "Stand-alone-Server-Bestätigung", "onboarding.form.standaloneServerForm.servicesUnavailable": "Einige der Services werden nicht verfügbar sein oder erfordern eine manuelle Einrichtung", "onboarding.form.standaloneServerForm.publishOwnApp": "Um Push-Benachrichtigungen zu senden, müssen Sie Ihre eigene App kompilieren und in Google Play und im App Store veröffentlichen", - "onboarding.form.standaloneServerForm.manuallyIntegrate": "Manuelle Integration mit externen Services erforderlich", - "Login_Detected": "Anmeldung erkannt", - "Logged_In_Via": "Eingeloggt über", - "Access_Your_Account": "Zugang zu Ihrem Konto", - "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Oder kopieren Sie diese URL und fügen Sie sie in eine Registerkarte Ihres Browsers ein", - "Thank_You_For_Choosing_RocketChat": "Danke, dass Sie sich für Rocket.Chat entschieden haben!", - "Device_Management_Client": "Client", - "Device_Management_OS": "OS", - "Device_Management_Device": "Gerät", - "Device_Management_Device_Unknown": "Unbekannt", - "Device_Management_IP": "IP", - "Device_Management_Email_Subject": "[Site_Name] - Anmeldung erkannt", - "Device_Management_Email_Body": "Sie können die folgenden Platzhalter verwenden: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>" + "onboarding.form.standaloneServerForm.manuallyIntegrate": "Manuelle Integration mit externen Services erforderlich" } \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json index a46d017c83e..fa84cd89e98 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/el.i18n.json @@ -1149,7 +1149,6 @@ "File_uploaded": "Το αÏχείο μεταφοÏτώθηκε", "files": "αÏχεία", "Files_only": "ΑφαιÏÎστε μόνο τα συνημμÎνα αÏχεία, κÏατήστε τα μηνÏματα", - "files_pruned": "τα αÏχεία κλαδεÏονται", "FileUpload": "ΑνÎβασμα αÏχείου", "FileUpload_Disabled": "Οι μεταφοÏτώσεις αÏχείων είναι απενεÏγοποιημÎνες.", "FileUpload_Enabled": "ΕνεÏγοποιηθεί η μεταφόÏτωση αÏχείων", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 5e1b906f408..fd1c2a8de6e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -25,6 +25,7 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "A new owner will be assigned automatically to <span style=\"font-weight: bold;\">__count__</span> rooms.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "A new owner will be assigned automatically to the <span style=\"font-weight: bold;\">__roomName__</span> room.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "A new owner will be assigned automatically to those <span style=\"font-weight: bold;\">__count__</span> rooms:<br/> __rooms__.", + "Accept_Call": "Accept Call", "Accept": "Accept", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accept incoming omnichannel requests even if there are no online agents", "Accept_new_livechats_when_agent_is_idle": "Accept new omnichannel requests when the agent is idle", @@ -2103,6 +2104,7 @@ "FEDERATION_Unique_Id_Description": "This is your federation unique ID, used to identify your peer on the mesh.", "Federation_Matrix": "Federation V2", "Federation_Matrix_enabled": "Enabled", + "FileUpload_Disabled_for_federation": "File uploads are disabled for Federated rooms.", "Federation_Matrix_Enabled_Alert": "<a target=\"_blank\" href=\"https://go.rocket.chat/i/matrix-federation\">More Information about Matrix Federation support can be found here</> (After any configuration, a restart is required to the changes take effect)", "Federation_Matrix_Federated": "Federated", "Federation_Matrix_Federated_Description": "By creating a federated room you'll not be able to enable encryption nor broadcast", @@ -2286,6 +2288,8 @@ "Global_Search": "Global search", "Go_to_your_workspace": "Go to your workspace", "Google_Play": "Google Play", + "Hold_Call": "Hold Call", + "Hold_Call_EE_only": "Hold Call (Enterprise Edition only)", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Service account key JSON file. More information can be found [here](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Google Tag Manager Id", @@ -3845,7 +3849,7 @@ "Receive_Group_Mentions": "Receive @all and @here mentions", "Receive_login_notifications": "Receive login notifications", "Receive_Login_Detection_Emails": "Receive login detection emails", - "Receive_Login_Detection_Emails_Description":"Receive an email each time a new login is detected on your account.", + "Receive_Login_Detection_Emails_Description": "Receive an email each time a new login is detected on your account.", "Recent_Import_History": "Recent Import History", "Record": "Record", "recording": "recording", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json index 5147fd46560..7c3e49acd04 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/eo.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "Dosiero alÅutita", "files": "dosierojn", "Files_only": "Nur forigu la kunaĵojn, konservu mesaÄojn", - "files_pruned": "dosieroj pruntitaj", "FileUpload": "Dosiero AlÅuto", "FileUpload_Disabled": "Dosieraj alÅutoj estas malebligitaj.", "FileUpload_Enabled": "Dosiero AlÅutoj Enabled", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 9e66920974a..ab823240fec 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nuevo propietario se asignará automáticamente a <span style=\"font-weight: bold;\">__count__</span> salas.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nuevo propietario se asignará automáticamente a la sala <span style=\"font-weight: bold;\">__roomName__</span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuevo propietario se asignará automáticamente a esas <span style=\"font-weight: bold;\">__count__</span> salas. <br/> __rooms__.", - "Accept": "Aceptar", "Accept_Call": "Aceptar la llamada", + "Accept": "Aceptar", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceptar solicitudes de Omnichannel entrantes aunque no haya agentes en lÃnea", "Accept_new_livechats_when_agent_is_idle": "Aceptar solicitudes de Omnichannel nuevas cuando el agente esté inactivo", "Accept_with_no_online_agents": "Aceptar sin agentes en lÃnea", @@ -1948,7 +1948,6 @@ "files": "archivos", "Files": "Archivos", "Files_only": "Eliminar solo los archivos adjuntos, mantener mensajes", - "files_pruned": "archivos retirados", "FileSize_Bytes": "__fileSize__ bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2087,6 +2086,7 @@ "Global_purge_override_warning": "Hay una polÃtica de retención global implementada. Si dejas desactivada la opción \"Anular la polÃtica de retención global\", solo podrás aplicar una polÃtica más restrictiva que la polÃtica global.", "Global_Search": "Búsqueda global", "Go_to_your_workspace": "Ir a tu espacio de trabajo", + "Hold_Call": "Llamada en espera", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Archivo JSON de clave de cuenta de servicio. Puedes encontrar más información [here] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "ID de Google Tag Manager", @@ -2131,7 +2131,6 @@ "Highlights_List": "Resaltar palabras", "History": "Historial", "Hold_Time": "Tiempo de espera", - "Hold_Call": "Llamada en espera", "Home": "Inicio", "Host": "Host", "Hospitality_Businness": "Negocio hostelero", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json index 44a0bff63ba..cc8c19ad893 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -1399,7 +1399,6 @@ "File_uploaded": "Ùایل آپلود شد", "files": "Ùایل ها", "Files_only": "Ùایل های متصل را Øذ٠کنید، پیام ها را Ù†Ú¯Ù‡ دارید", - "files_pruned": "Ùایل های Ùشرده شده", "FileUpload": "آپلود Ùایل", "FileUpload_Disabled": "آپلود Ùایل غیرÙعال است", "FileUpload_Enabled": "آپلود Ùایل Ùعال شد", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 93bbece60ad..f5bb5f563f5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -1143,7 +1143,6 @@ "File_uploaded": "Tiedosto ladattu", "files": "tiedostot", "Files_only": "Poista vain liitetyt tiedostot, pidä viestejä", - "files_pruned": "tiedostoja leikattu", "FileUpload": "Lähetä tiedosto", "FileUpload_Disabled": "Tiedoston lataukset on poistettu käytöstä.", "FileUpload_Enabled": "Tiedostojen lähetykset käytössä", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index a3933314a12..fbd005b2606 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nouveau propriétaire sera automatiquement attribué à <span style=\"font-weight: bold;\">__count__</span> salons.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nouveau propriétaire sera automatiquement assigné au salon <span style=\"font-weight: bold;\">__roomName__</span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nouveau propriétaire sera automatiquement assigné à ces <span style=\"font-weight: bold;\">__count__</span> salons :<br/>__rooms__.", - "Accept": "Accepter", "Accept_Call": "Accepter l'appel", + "Accept": "Accepter", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demandes omnicanales entrantes même si il n'y a pas d'agent en ligne", "Accept_new_livechats_when_agent_is_idle": "Accepter les nouvelles demandes omnicanales lorsque l'agent est inactif", "Accept_with_no_online_agents": "Accepter sans agents en ligne", @@ -1956,7 +1956,6 @@ "files": "fichiers", "Files": "Fichiers", "Files_only": "Ne supprimer que les fichiers joints, conserver les messages", - "files_pruned": "fichiers élagués", "FileSize_Bytes": "__fileSize__ octets", "FileSize_KB": "__fileSize__ ko", "FileSize_MB": "__fileSize__ Mo", @@ -2096,6 +2095,7 @@ "Global_purge_override_warning": "Une politique de rétention globale est en place. Si vous laissez l'option \"Ignorer la politique de rétention globale\" désactivée, vous ne pouvez appliquer qu'une politique plus stricte que la politique globale.", "Global_Search": "Recherche globale", "Go_to_your_workspace": "Accéder à votre espace de travail", + "Hold_Call": "Mettre l'appel en attente", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Fichier JSON de clé de compte de service. Plus d'informations sont disponibles [here] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "ID Google Tag Manager", @@ -2140,7 +2140,6 @@ "Highlights_List": "Mots en vedette", "History": "Historique", "Hold_Time": "Temps d'attente", - "Hold_Call": "Mettre l'appel en attente", "Home": "Accueil", "Host": "Hôte", "Hospitality_Businness": "Hôtellerie", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json index 8a9e5bf662a..83b2ee3f85b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -1273,7 +1273,6 @@ "File_uploaded": "Datoteka je prenesena", "files": "slika", "Files_only": "Samo uklonite priložene datoteke, zadržite poruke", - "files_pruned": "datoteke su obrezane", "FileUpload": "Prijenos datoteke", "FileUpload_Disabled": "Prijenosi datoteka su onemogućeni", "FileUpload_Enabled": "Prijenos Datoteka Omogućeno", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 4bc2d9f70ef..d2082c411f0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Egy új tulajdonos automatikusan <span style=\"font-weight: bold;\">__count__</span> szobához lesz rendelve.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Egy új tulajdonos automatikusan <span style=\"font-weight: bold;\">__roomName__</span> szobához lesz rendelve.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Egy új tulajdonos lesz automatikusan hozzárendelve ezekhez a <span style=\"font-weight: bold;\">__count__</span> szobákhoz: <br/> __rooms__.", - "Accept": "Elfogadás", "Accept_Call": "HÃvás fogadása", + "Accept": "Elfogadás", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "BejövÅ‘ Livechat kérések elfogadása akkor is, ha nincs elérhetÅ‘ ügyintézÅ‘", "Accept_new_livechats_when_agent_is_idle": "Új Livechat kérések elfogadása, ha az ügyintézÅ‘ tétlen", "Accept_with_no_online_agents": "Elfogadás elérhetÅ‘ ügyintézÅ‘k nélkül", @@ -1644,7 +1644,6 @@ "files": "fájlok", "Files": "Fájlok", "Files_only": "Csak távolÃtsa el a csatolt fájlokat, tartsa meg az üzeneteket", - "files_pruned": "fájlok metszettek", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json index df85972d997..db36851ed60 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/id.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "File diunggah", "files": "file", "Files_only": "Hanya hapus file yang dilampirkan, simpan pesan", - "files_pruned": "file dipangkas", "FileUpload": "Unggah File", "FileUpload_Disabled": "Upload file dinonaktifkan", "FileUpload_Enabled": "Unggah File Dihidupkan", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index 2655e36530c..cccfec525f4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -23,8 +23,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nuovo proprietario verrà assegnato automaticamente a<span style=\"font-weight: bold;\">__count__</span>stanze.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nuovo proprietario verrà assegnato automaticamente alla stanza <span style=\"font-weight: bold;\">__roomName__</span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": null, - "Accept": "Accetta", "Accept_Call": "Accetta la chiamata", + "Accept": "Accetta", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accetta richieste livechat in arrivo anche se non c'è alcun operatore online", "Accept_new_livechats_when_agent_is_idle": "Accetta nuove richieste omnichannel quando l'operatore è inattivo", "Accept_with_no_online_agents": "Accetta con nessun operatore online", @@ -1191,7 +1191,6 @@ "files": "File", "Files": "File", "Files_only": "Rimuovere solo i file allegati, mantenere i messaggi", - "files_pruned": "file cancellati", "FileUpload": "Caricamento file", "FileUpload_Disabled": "Caricamento file non permesso.", "FileUpload_Enabled": "Caricamento file abilitato", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index fe60c65fefd..f48c32950ab 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "æ–°ã—ã„所有者ãŒ<span style=\"font-weight: bold;\">__count__</span>個ã®ãƒ«ãƒ¼ãƒ ã«è‡ªå‹•çš„ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¾ã™ã€‚", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "æ–°ã—ã„所有者ãŒ<span style=\"font-weight: bold;\">__roomName__</span>ã®ãƒ«ãƒ¼ãƒ ã«è‡ªå‹•çš„ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¾ã™ã€‚", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "æ–°ã—ã„所有者ãŒãれらã®<span style=\"font-weight: bold;\">__count__</span>個ã®ãƒ«ãƒ¼ãƒ ã«è‡ªå‹•çš„ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¾ã™ï¼š<br/> __rooms__ 。", - "Accept": "åŒæ„", "Accept_Call": "電話ã«å‡ºã‚‹", + "Accept": "åŒæ„", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "オンラインエージェントãŒå˜åœ¨ã—ãªã„å ´åˆã§ã‚‚ã€å—信オムニãƒãƒ£ãƒãƒ«è¦æ±‚ã‚’å—ã‘付ã‘ã‚‹", "Accept_new_livechats_when_agent_is_idle": "エージェントãŒã‚¢ã‚¤ãƒ‰ãƒ«çŠ¶æ…‹ã®å ´åˆã€æ–°ã—ã„オムニãƒãƒ£ãƒãƒ«è¦æ±‚ã‚’å—ã‘付ã‘ã‚‹", "Accept_with_no_online_agents": "オンラインエージェントãªã—ã§å—ã‘付ã‘ã‚‹", @@ -1949,7 +1949,6 @@ "files": "ファイル", "Files": "ファイル", "Files_only": "添付ファイルを削除ã—ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯ãã®ã¾ã¾æ®‹ã™", - "files_pruned": "æ•´ç†ã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«", "FileSize_Bytes": "__fileSize__ãƒã‚¤ãƒˆ", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2088,6 +2087,7 @@ "Global_purge_override_warning": "ã‚°ãƒãƒ¼ãƒãƒ«ãªä¿æŒãƒãƒªã‚·ãƒ¼ãŒå°Žå…¥ã•ã‚Œã¦ã„ã¾ã™ã€‚「グãƒãƒ¼ãƒãƒ«ä¿æŒãƒãƒªã‚·ãƒ¼ã‚’上書ãã€ã‚’オフã«ã—ã¦ãŠãã¨ã€ã‚°ãƒãƒ¼ãƒãƒ«ãƒãƒªã‚·ãƒ¼ã‚ˆã‚Šã‚‚厳ã—ã„ãƒãƒªã‚·ãƒ¼ã®ã¿ã‚’é©ç”¨ã§ãã¾ã™ã€‚", "Global_Search": "ã‚°ãƒãƒ¼ãƒãƒ«æ¤œç´¢", "Go_to_your_workspace": "ワークスペースã«ç§»å‹•", + "Hold_Call": "通話をä¿ç•™", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "サービスアカウントã‚ーã®JSONファイル。詳細ã¯[ã“ã¡ã‚‰](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Googleタグマãƒãƒ¼ã‚¸ãƒ£ãƒ¼ID", @@ -2132,7 +2132,6 @@ "Highlights_List": "å˜èªžã®å¼·èª¿è¡¨ç¤º", "History": "å±¥æ´", "Hold_Time": "ä¿ç•™æ™‚刻", - "Hold_Call": "通話をä¿ç•™", "Home": "ホーム", "Host": "ホスト", "Hospitality_Businness": "サービスæ¥", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index 4b5b0c88b42..61aed95140a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1385,7 +1385,6 @@ "File_uploaded": "ឯកសារបានផ្ទុកឡើង", "files": "ឯកសារ", "Files_only": "យកážáŸ‚ឯកសារភ្ជាប់ប៉ុណ្ណោះ, រក្សាទុកសារ", - "files_pruned": "ឯកសារដែលបានកាážáŸ‹áž…áŸáž‰", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 226b3990979..8ec1339e3af 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -1699,7 +1699,6 @@ "files": "파ì¼", "Files": "íŒŒì¼ ëª©ë¡", "Files_only": "첨부 íŒŒì¼ ë§Œ ì œê±°í•˜ê³ ë©”ì‹œì§€ ë³´ê´€", - "files_pruned": "íŒŒì¼ ì •ë¦¬ë¥¼ 완료했습니다.", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__KB", "FileSize_MB": "__fileSize__KB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json index 5bb87b60984..c51ff42038a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -1139,7 +1139,6 @@ "File_uploaded": "Pelê barkirin", "files": "pelan", "Files_only": "Tenê pelên girêdan veÅŸartin, peyamên xwe biparêzin", - "files_pruned": "pelên hilberandin", "FileUpload": "Wêne Upload", "FileUpload_Disabled": "Upload uploads disabled.", "FileUpload_Enabled": "Uploads File çalake", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json index 2017a1ac65b..5ac91d50102 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -1175,7 +1175,6 @@ "File_uploaded": "File uploaded", "files": "ໄຟລ໌", "Files_only": "ພຽງà»àº•à»ˆà»€àºàº»àº²à»„ຟລ໌ທີ່ຕິດຄັດໄວ້, ເàºàº±àºšàº‚à»à»‰àº„ວາມ", - "files_pruned": "ໄຟລ໌ຖືàºàº•àº±àº”àºàºàº", "FileUpload": "Upload file", "FileUpload_Disabled": "àºàº±àºšà»‚ຫລດໄຟລ໌à»àº¡à»ˆàº™àº–ືàºàº›àº´àº”ໃຊ້ງານ.", "FileUpload_Enabled": "àºàº±àºšà»‚ຫຼດໄຟລ໌ທີ່ເປີດໃຊ້ວຽàº", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json index b39113c81f9..7016a1a1d92 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lt.i18n.json @@ -1197,7 +1197,6 @@ "File_uploaded": "Ä®keltas failas", "files": "failai", "Files_only": "Tik paÅ¡alinkite prisegtus failus, saugokite praneÅ¡imus", - "files_pruned": "failai suskaidomi", "FileUpload": "Failo ikelimas", "FileUpload_Disabled": "Failų įkÄ—limai iÅ¡jungti.", "FileUpload_Enabled": "Failų įkÄ—limai įjungti", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json index d86a496e24c..6e1f455dac2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/lv.i18n.json @@ -1154,7 +1154,6 @@ "File_uploaded": "Fails augÅ¡upielÄdÄ“ts", "files": "faili", "Files_only": "Noemt tikai pievienotos failus, saglabÄt ziņojumus", - "files_pruned": "faili ir apgriezti", "FileUpload": "Faila augÅ¡upielÄde", "FileUpload_Disabled": "Failu augÅ¡upielÄde ir atspÄ“jota.", "FileUpload_Enabled": "IespÄ“jota failu augÅ¡upielÄde", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json index f635fe00420..549d995d36a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -1138,7 +1138,6 @@ "File_uploaded": "Файл байршуулÑан", "files": "файлууд", "Files_only": "Зөвхөн хавÑаргаÑан файлуудыг уÑтгах, меÑÑежүүдийг хадгалах", - "files_pruned": "файлууд нь ÑалгагдÑан", "FileUpload": "Файл оруулах", "FileUpload_Disabled": "Файл байршуулалтыг идÑвхгүй болгоÑон.", "FileUpload_Enabled": "Файл оруулах нь идÑвхжÑÑн", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index 9a3e2ecc4c3..af297d11308 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1140,7 +1140,6 @@ "File_uploaded": "Fail dimuat naik", "files": "fail", "Files_only": "Hanya keluarkan fail yang dilampirkan, simpan mesej", - "files_pruned": "fail dipangkas", "FileUpload": "Muat Naik Fail", "FileUpload_Disabled": "Muat naik fail dilumpuhkan.", "FileUpload_Enabled": "Naik Fail Diaktifkan", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index b46e1ec23b4..8644720510d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Een nieuwe eigenaar wordt automatisch toegewezen aan <span style=\"font-weight: bold;\">__count__</span> kamers.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Een nieuwe eigenaar wordt automatisch toegewezen aan de <span style=\"font-weight: bold;\">__roomName__</span> kamer.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Een nieuwe eigenaar wordt automatisch toegewezen aan de <span style=\"font-weight: bold;\">__count__</span> kamers: <br/> __rooms__.", - "Accept": "Accepteren", "Accept_Call": "Oproep accepteren", + "Accept": "Accepteren", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepteer inkomende omnichannel-verzoeken, zelfs als er geen online agenten zijn", "Accept_new_livechats_when_agent_is_idle": "Accepteer nieuwe omnichannel-verzoeken wanneer de agent niet actief is", "Accept_with_no_online_agents": "Accepteer zonder online agenten", @@ -1956,7 +1956,6 @@ "files": "bestanden", "Files": "Bestanden", "Files_only": "Verwijder alleen de bijgevoegde bestanden, bewaar berichten", - "files_pruned": "bestanden gesnoeid", "FileSize_Bytes": "__fileSize__ bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2096,6 +2095,7 @@ "Global_purge_override_warning": "Er is een globaal retentiebeleid ingesteld. Als u 'Globaal bewaarbeleid negeren' uitschakelt, kunt u alleen een beleid toepassen dat strenger is dan het globale beleid.", "Global_Search": "Globaal zoeken", "Go_to_your_workspace": "Ga naar je werkruimte", + "Hold_Call": "Gesprek in de wacht zetten", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "JSON-bestand serviceaccountsleutel. Meer informatie is [hier](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account) te vinden", "GoogleTagManager_id": "Google Tag Manager Id", @@ -2140,7 +2140,6 @@ "Highlights_List": "Highlight woorden", "History": "Geschiedenis", "Hold_Time": "Wachttijd", - "Hold_Call": "Gesprek in de wacht zetten", "Home": "Home", "Host": "Host", "Hospitality_Businness": "Horeca", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json index 9702a44f9db..de7c5ca320f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1212,7 +1212,6 @@ "File_uploaded": "Fil opplastet", "files": "filer", "Files_only": "Bare fjern vedlagte filer, hold meldinger", - "files_pruned": "filer beskjæres", "FileUpload": "Filopplasting", "FileUpload_Disabled": "Filopplastinger er deaktivert.", "FileUpload_Enabled": "Filopplastinger aktivert", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 1e7f652775f..4fe2f0204ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -23,13 +23,14 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Nowy wÅ‚aÅ›ciciel zostanie automatycznie przypisany do <span style=\"font-weight: bold;\">__count__</span> pokoi.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Nowy wÅ‚aÅ›ciciel zostanie automatycznie przypisany do pokoju o nazwie <span style=\"font-weight: bold;\">__roomName__</span>", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Nowy wÅ‚aÅ›ciciel zostanie automatyczne przypisany do tych <span style=\"font-weight: bold;\">__count__</span> pokoi:<br/> __rooms__.", - "Accept": "Akceptuj", "Accept_Call": "Odbierz poÅ‚Ä…czenie", + "Accept": "Akceptuj", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Akceptuj przychodzÄ…ce żądania wielokanaÅ‚owe, nawet gdy brak agentów online", "Accept_new_livechats_when_agent_is_idle": "Akceptuj nowe żądania wielokanaÅ‚owe, gdy agent jest bezczynny", "Accept_with_no_online_agents": "Akceptuj, nawet gry brak agentów online", "Access_not_authorized": "DostÄ™p zabroniony", "Access_Token_URL": "URL dostÄ™pu przez token", + "Access_Your_Account": "DostÄ™p do konta", "access-mailer": "Uzyskaj dostÄ™p do ekranu Mailer", "access-mailer_description": "Uprawnienie do wysyÅ‚ania masowych wiadomoÅ›ci e-mail do wszystkich użytkowników.", "access-permissions": "Ekran uprawnieÅ„ dostÄ™pu", @@ -1522,6 +1523,13 @@ "Device_Changes_Not_Available": "Zmiany w urzÄ…dzeniach niedostÄ™pne w tej przeglÄ…darce. Aby uzyskać gwarancjÄ™ dostÄ™pnoÅ›ci, skorzystaj z oficjalnej aplikacji desktopowej Rocket.Chat.", "Device_Changes_Not_Available_Insecure_Context": "Zmiany w urzÄ…dzeniach sÄ… dostÄ™pne tylko w bezpiecznych kontekstach (np. https://)", "Device_Management": "ZarzÄ…dzanie urzÄ…dzeniami", + "Device_Management_Client": "Klient", + "Device_Management_Device": "UrzÄ…dzenie", + "Device_Management_Device_Unknown": "Nieznane", + "Device_Management_Email_Subject": "[Site_Name] - Login wykryty", + "Device_Management_Email_Body": "Możesz użyć nastÄ™pujÄ…cych placeholderów: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>", + "Device_Management_IP": "IP", + "Device_Management_OS": "OS", "Device_ID": "Identyfikator urzÄ…dzenia", "Device_Info": "Informacje o urzÄ…dzeniu", "Device_Logged_Out": "UrzÄ…dzenie wylogowane", @@ -2077,6 +2085,7 @@ "FEDERATION_Unique_Id_Description": "Jest to unikalny identyfikator Federation, używany do identyfikacji twojego peer'a na mesh.", "Federation_Matrix": "Federacja V2", "Federation_Matrix_enabled": "WÅ‚Ä…czone", + "FileUpload_Disabled_for_federation": "PrzesyÅ‚anie plików jest wyÅ‚Ä…czone dla kanałów sfederowanych.", "Federation_Matrix_Enabled_Alert": "Wsparcie Federacji Matrix jest w wersji alfa. Stosowanie w systemie produkcyjnym nie jest obecnie zalecane.<a target=\"_blank\" href=\"https://go.rocket.chat/i/matrix-federation\">WiÄ™cej informacji na temat obsÅ‚ugi Matrix Federation można znaleźć tutaj</>", "Federation_Matrix_Federated": "Sfederowany", "Federation_Matrix_Federated_Description": "TworzÄ…c pokój federacyjny nie bÄ™dziesz mógÅ‚ wÅ‚Ä…czyć szyfrowania ani rozgÅ‚aszania", @@ -2113,8 +2122,6 @@ "files": "akta", "Files": "Pliki", "Files_only": "Usuwaj tylko zaÅ‚Ä…czone pliki, zachowaj wiadomoÅ›ci", - "Files_unavailable_for_federation": "Pliki sÄ… niedostÄ™pne dla kanałów sfederowanych", - "files_pruned": "pliki zostaÅ‚y przyciÄ™te", "FileSize_Bytes": "__fileSize__ Bajty", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2122,7 +2129,6 @@ "FileUpload_Description": "Skonfiguruj przesyÅ‚anie i przechowywanie plików.", "FileUpload_Cannot_preview_file": "Nie można wyÅ›wietlić podglÄ…du pliku", "FileUpload_Disabled": "PrzesyÅ‚anie plików jest wyÅ‚Ä…czone.", - "FileUpload_Disabled_for_federation": "PrzesyÅ‚anie plików jest wyÅ‚Ä…czone dla kanałów sfederowanych.", "FileUpload_Enable_json_web_token_for_files": "WÅ‚Ä…czenie Json Web Tokens protection do przesyÅ‚ania plików", "FileUpload_Enable_json_web_token_for_files_description": "Dodaje JWT do adresów url wysyÅ‚anych plików", "FileUpload_Enabled": "PrzesyÅ‚anie plików wÅ‚Ä…czone", @@ -2262,6 +2268,8 @@ "Global_Search": "Wyszukiwanie globalne", "Go_to_your_workspace": "Idź do swojej przestrzeni roboczej", "Google_Play": "Google Play", + "Hold_Call": "ZawieÅ› poÅ‚Ä…czenie", + "Hold_Call_EE_only": "Zawieszenie poÅ‚Ä…czenia (tylko wersja Enterprise)", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Plik JSON klucza usÅ‚ugi. WiÄ™cej informacji można znaleźć [tutaj] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Menedżer tagów Google Id", @@ -2309,8 +2317,6 @@ "Highlights_List": "SÅ‚owa ustawione jako wzmianka", "History": "Historia", "Hold_Time": "Czas zawieszenia", - "Hold_Call": "ZawieÅ› poÅ‚Ä…czenie", - "Hold_Call_EE_only": "Zawieszenie poÅ‚Ä…czenia (tylko wersja Enterprise)", "Home": "Dom", "Homepage": "Strona główna", "Host": "Host", @@ -2959,6 +2965,8 @@ "Logged_out_of_other_clients_successfully": "Wylogowanie z innymi klientów powiodÅ‚o siÄ™", "Login": "Login", "Login_Attempts": "Nieudane próby logowania", + "Login_Detected": "Wykryto logowanie", + "Logged_In_Via": "Zalogowano siÄ™ za poÅ›rednictwem", "Login_Logs": "Logi logowania", "Login_Logs_ClientIp": "Pokaż IP klienta przy nieudanych próbach logowania", "Login_Logs_Enabled": "Loguj (na konsoli) nieudane próby logowania", @@ -3548,6 +3556,7 @@ "optional": "opcjonalne", "Options": "Opcje", "or": "lub", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Możesz też skopiować i wkleić ten adres w zakÅ‚adce swojej przeglÄ…darki", "Or_talk_as_anonymous": "Lub porozmawiaj jako anonimowy", "Order": "Kolejność", "Organization_Email": "E-mail organizacji", @@ -3624,6 +3633,7 @@ "Phone_call": "PoÅ‚Ä…czenie telefoniczne", "Phone_Number": "Numer telefonu", "Thank_you_exclamation_mark": "DziÄ™ki!", + "Thank_You_For_Choosing_RocketChat": "DziÄ™kujemy za wybranie Rocket.Chat!", "Phone_already_exists": "Telefon już istnieje", "Phone_number": "Numer telefonu", "PID": "PID", @@ -5374,17 +5384,5 @@ "onboarding.form.standaloneServerForm.title": "Potwierdzenie serwera standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usÅ‚ug bÄ™dÄ… niedostÄ™pne lub bÄ™dÄ… wymagaÅ‚y rÄ™cznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyÅ‚ania powiadomieÅ„ push należy skompilować i opublikować wÅ‚asnÄ… aplikacjÄ™ w Google Play i App Store", - "onboarding.form.standaloneServerForm.manuallyIntegrate": "Konieczność rÄ™cznej integracji z usÅ‚ugami zewnÄ™trznymi", - "Login_Detected": "Wykryto logowanie", - "Logged_In_Via": "Zalogowano siÄ™ za poÅ›rednictwem", - "Access_Your_Account": "DostÄ™p do konta", - "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "Możesz też skopiować i wkleić ten adres w zakÅ‚adce swojej przeglÄ…darki", - "Thank_You_For_Choosing_RocketChat": "DziÄ™kujemy za wybranie Rocket.Chat!", - "Device_Management_Client": "Klient", - "Device_Management_OS": "OS", - "Device_Management_Device": "UrzÄ…dzenie", - "Device_Management_Device_Unknown": "Nieznane", - "Device_Management_IP": "IP", - "Device_Management_Email_Subject": "[Site_Name] - Login wykryty", - "Device_Management_Email_Body": "Możesz użyć nastÄ™pujÄ…cych placeholderów: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>" + "onboarding.form.standaloneServerForm.manuallyIntegrate": "Konieczność rÄ™cznej integracji z usÅ‚ugami zewnÄ™trznymi" } \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6d76ed6eafe..ca2bbeed878 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Um novo proprietário será atribuÃdo automaticamente a <span style=\"font-weight: bold;\">__count__</span> salas.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Um novo proprietário será atribuÃdo à sala <span style=\"font-weight: bold;\">__roomName__</span>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Um novo proprietário será atribuÃdo automaticamente a estas <span style=\"font-weight: bold;\">__count__</span> salas:<br/> __rooms__.", - "Accept": "Aceitar", "Accept_Call": "Aceitar chamada", + "Accept": "Aceitar", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar solicitações de omnichannel de entrada mesmo que não tenham agentes online", "Accept_new_livechats_when_agent_is_idle": "Aceitar novas solicitações de omnichannel quando o agente estiver inativo", "Accept_with_no_online_agents": "Aceitar sem agentes online", @@ -1960,7 +1960,6 @@ "files": "arquivos", "Files": "Arquivos", "Files_only": "Apenas remova os arquivos anexados, mantenha mensagens", - "files_pruned": "arquivos removidos", "FileSize_Bytes": "__fileSize__ bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2102,6 +2101,7 @@ "Global_purge_override_warning": "Uma polÃtica de retenção global está em vigor. Se deixar de fora \"Substituir diretiva de retenção global\", você só poderá aplicar uma diretiva mais rÃgida do que a diretiva global.", "Global_Search": "Pesquisa global", "Go_to_your_workspace": "Vá para o seu espaço de trabalho", + "Hold_Call": "Pausar chamada", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Arquivo JSON da chave da conta de serviço. Mais informações podem ser encontradas [aqui] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "ID Google Tag Manager", @@ -4735,10 +4735,10 @@ "Visitor_page_URL": "URL da página do visitante", "Visitor_time_on_site": "Tempo do visitante no site", "Voice_Call": "Chamada de voz", - "VoIP_Enabled": "Canal de voz habilitado", - "VoIP_Enabled_Description": "Conecte agentes e clientes através de chamadas de voz", "VoIP_Enable_Keep_Alive_For_Unstable_Networks": "SIP Options Keep Alive habilitado", "VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description": "Monitore o status de múltiplos gateways SIP externos enviando mensagens SIP OPTIONS periódicas. Usado para redes instáveis.", + "VoIP_Enabled": "Canal de voz habilitado", + "VoIP_Enabled_Description": "Conecte agentes e clientes através de chamadas de voz", "VoIP_Extension": "Extensão de VoIP", "Voip_Server_Configuration": "Servidor Websocket Asterisk", "VoIP_Server_Websocket_Port": "Porta do webSocket", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 1e3683f9da5..95447f34cf4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1373,7 +1373,6 @@ "File_uploaded": "Ficheiro carregado", "files": "ficheiros", "Files_only": "Apenas remova os arquivos anexados, mantenha as mensagens", - "files_pruned": "ficheiros removidos", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ KB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json index 6d947de2d27..e74b3213b33 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -1141,7 +1141,6 @@ "File_uploaded": "FiÈ™ier încărcat", "files": "fiÈ™iere", "Files_only": "EliminaÈ›i numai fiÈ™ierele ataÈ™ate, păstraÈ›i mesajele", - "files_pruned": "fiÈ™iere tăiate", "FileUpload": "ÃŽncărcare fiÈ™ier", "FileUpload_Disabled": "ÃŽncărcările de fiÈ™iere sunt dezactivate.", "FileUpload_Enabled": "ÃŽncărcarea de fiÈ™iere activată", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 2da1a671e93..b25ae72d015 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Ðовый владелец будет автоматичеÑки назначен на <span style=\"font-weight: bold;\">__count__</span> чатов.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Ðовый владелец будет автоматичеÑки назначен Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ð° <span0>__roomName__</span0>.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Ðовый владелец будет автоматичеÑки назначен Ñтим <span style=\"font-weight: bold;\">__count__</span> чатам:<br/> __rooms__.", - "Accept": "ПринÑÑ‚ÑŒ", "Accept_Call": "ПринÑÑ‚ÑŒ вызов", + "Accept": "ПринÑÑ‚ÑŒ", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Принимать входÑщие запроÑÑ‹ Ñ livechat, даже еÑли нет подключенных Ñотрудников", "Accept_new_livechats_when_agent_is_idle": "Принимайте новые запроÑÑ‹ livechat, когда агент проÑтаивает", "Accept_with_no_online_agents": "Принимать Ñ Ð½ÐµÐ¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ‹Ð¼Ð¸ Ñотрудниками", @@ -1971,7 +1971,6 @@ "files": "файлы", "Files": "Файлы", "Files_only": "УдалÑÑ‚ÑŒ только прикрепленные файлы. Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾ÑтанутÑÑ", - "files_pruned": "файлы удалены", "FileSize_Bytes": "__fileSize__ байт", "FileSize_KB": "__fileSize__ КБ", "FileSize_MB": "__fileSize__ МБ", @@ -2111,6 +2110,8 @@ "Global_purge_override_warning": "ДейÑтвует Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿Ð¾Ð»Ð¸Ñ‚Ð¸ÐºÐ° очиÑтки. ЕÑли вы оÑтавите опцию \"Переопределить глобальную политику очиÑтки\" отключенной, вы можете применить политику, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑвлÑетÑÑ Ð±Ð¾Ð»ÐµÐµ Ñтрогой, чем Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿Ð¾Ð»Ð¸Ñ‚Ð¸ÐºÐ°.", "Global_Search": "Общий поиÑк", "Go_to_your_workspace": "Перейти в рабочее проÑтранÑтво", + "Hold_Call": "Удерживать вызов", + "Hold_Call_EE_only": "Удержание вызова (Только Ð´Ð»Ñ Ð•Ð•)", "GoogleCloudStorage": "Google Cloud Storage", "GoogleNaturalLanguage_ServiceAccount_Description": "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ ключа JSON файла. Больше информации можно найти здеÑÑŒ [here](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Идентификатор Google Tag Manager ", @@ -2155,8 +2156,6 @@ "Highlights_List": "ПодÑвечивать Ñлова", "History": "ИÑториÑ", "Hold_Time": "Ð’Ñ€ÐµÐ¼Ñ ÑƒÐ´ÐµÑ€Ð¶Ð°Ð½Ð¸Ñ", - "Hold_Call": "Удерживать вызов", - "Hold_Call_EE_only": "Удержание вызова (Только Ð´Ð»Ñ Ð•Ð•)", "Home": "ГлавнаÑ", "Host": "ХоÑÑ‚", "Hospitality_Businness": "ГоÑтиничный бизнеÑ", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json index e3e0bec459e..201a32ab0bb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sk-SK.i18n.json @@ -1152,7 +1152,6 @@ "File_uploaded": "Nahraný súbor", "files": "súbory", "Files_only": "Odpojte iba pripojené súbory, uložte správy", - "files_pruned": "súbory prerezané", "FileUpload": "Nahranie súboru", "FileUpload_Disabled": "Nahrávanie súborov je zakázané.", "FileUpload_Enabled": "Odovzdané súbory sú povolené", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json index 0b2a560b177..6676bd5ba24 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sl-SI.i18n.json @@ -1133,7 +1133,6 @@ "File_uploaded": "Datoteka naložena", "files": "datoteke", "Files_only": "Odstranite le priložene datoteke in hranite sporoÄila", - "files_pruned": "datoteke obrezane", "FileUpload": "Nalaganje datotek", "FileUpload_Disabled": "Nalaganje datotek je onemogoÄeno. ", "FileUpload_Enabled": "Nalaganje datotek je omogoÄeno", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json index 9ae5373a440..2beedf45d13 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -1143,7 +1143,6 @@ "File_uploaded": "Skedari u ngarkua", "files": "fotografi", "Files_only": "Hiqni vetëm dosjet e bashkangjitura, mbajini mesazhet", - "files_pruned": "fotografi të shkurtuara", "FileUpload": "Ngarko skedarin", "FileUpload_Disabled": "Ngarkimet e skedarëve janë të çaktivizuara.", "FileUpload_Enabled": "Ngarkimet fotografi Enabled", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json index f8600df6ff9..a699ca0e3c6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -1029,7 +1029,6 @@ "File_uploaded": "Датотека је отпремљена", "files": "фајлови", "Files_only": "Само уклоните приложене датотеке, задржите поруке", - "files_pruned": "датотеке обрезане", "FileUpload": "Отпремање датотека", "FileUpload_Disabled": "Онемогућено је поÑтављање датотека.", "FileUpload_Enabled": "Отпремање датотека је омогућено", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 5acd2f322d1..e849017ab26 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -1211,7 +1211,6 @@ "File_uploaded": "Uppladdad fil", "files": "filer", "Files_only": "Ta bara bort bifogade filer, behÃ¥ll meddelanden", - "files_pruned": "filer beskurna", "FileUpload": "Uppladdad fil", "FileUpload_Disabled": "Filuppladdningar är inaktiverade.", "FileUpload_Enabled": "Filuppladdningar aktiverade", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index b20409b768f..76daa060171 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -1142,7 +1142,6 @@ "File_uploaded": "கோபà¯à®ªà¯ பதிவேறà¯à®±à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯", "files": "கோபà¯à®ªà¯à®•à®³à¯ˆ", "Files_only": "இணைகà¯à®•à®ªà¯à®ªà®Ÿà¯à®Ÿ கோபà¯à®ªà¯à®•à®³à¯ˆ மடà¯à®Ÿà¯à®®à¯‡ நீகà¯à®•, செயà¯à®¤à®¿à®•à®³à¯ˆ வைதà¯à®¤à®¿à®°à¯à®•à¯à®•à®µà¯à®®à¯", - "files_pruned": "கோபà¯à®ªà¯à®•à®³à¯ˆ சீரமைகà¯à®•", "FileUpload": "கோபà¯à®ªà¯ பதிவேறà¯à®±à®®à¯", "FileUpload_Disabled": "கோபà¯à®ªà¯ பதிவேறà¯à®±à®™à¯à®•à®³à¯ à®®à¯à®Ÿà®•à¯à®•à®ªà¯à®ªà®Ÿà¯à®Ÿà¯à®³à¯à®³à®©.", "FileUpload_Enabled": "கோபà¯à®ªà¯ பதிவேறà¯à®±à®™à¯à®•à®³à¯ இயகà¯à®•à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json index 5463563e618..fa5a7a73c7e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/th-TH.i18n.json @@ -1138,7 +1138,6 @@ "File_uploaded": "à¸à¸±à¸›à¹‚หลดไฟล์à¹à¸¥à¹‰à¸§", "files": "ไฟล์", "Files_only": "ลบเฉพาะไฟล์ที่à¹à¸™à¸šà¹€à¸à¹‡à¸šà¸‚้à¸à¸„วามไว้", - "files_pruned": "ไฟล์ pruned", "FileUpload": "à¸à¸±à¸›à¹‚หลดไฟล์", "FileUpload_Disabled": "à¸à¸²à¸£à¸à¸±à¸›à¹‚หลดไฟล์ถูà¸à¸›à¸´à¸”ใช้งาน", "FileUpload_Enabled": "ไฟล์ที่à¸à¸±à¸›à¹‚หลดถูà¸à¹€à¸›à¸´à¸”ใช้งานà¹à¸¥à¹‰à¸§", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index 055d7de9261..601c1ff8b68 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -1392,7 +1392,6 @@ "files": "dosyalar", "Files": "Dosyalar", "Files_only": "Sadece ekli dosyaları kaldırın, mesajları saklayın", - "files_pruned": "dosyalar budanmış", "FileSize_Bytes": "__fileSize__ Bayt", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json index d1d43bae674..d6780fc774e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -1528,7 +1528,6 @@ "files": "файли", "Files": "Файли", "Files_only": "ВидалÑти лише файли, а Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð»Ð¸ÑˆÐ¸Ñ‚Ð¸", - "files_pruned": "файли обрізані", "FileSize_Bytes": "__fileSize__ Байт", "FileSize_KB": "__fileSize__ Кб", "FileSize_MB": "__fileSize__ Мб", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json index 33efc075367..a974223a938 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/vi-VN.i18n.json @@ -1237,7 +1237,6 @@ "File_uploaded": "File đã được tải lên", "files": "các táºp tin", "Files_only": "Chỉ xóa các tệp được Ä‘Ãnh kèm, giữ thÆ°", - "files_pruned": "táºp tin bị cắt xén", "FileUpload": "Tải tệp lên", "FileUpload_Disabled": "Tải tệp lên bị vô hiệu.", "FileUpload_Enabled": "Äã báºt", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index 77193c8eeb3..8ee131e48d4 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -1162,7 +1162,6 @@ "files": "æ¡£", "Files": "文件", "Files_only": "åªåˆ é™¤é™„åŠ çš„æ–‡ä»¶ï¼Œä¿ç•™æ¶ˆæ¯", - "files_pruned": "修剪过的文件", "FileUpload": "ä¸Šä¼ æ–‡ä»¶", "FileUpload_Disabled": "æ–‡ä»¶ä¸Šä¼ åŠŸèƒ½è¢«ç¦ç”¨ã€‚", "FileUpload_Enabled": "æ–‡ä»¶ä¸Šä¼ å·²å¯ç”¨", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index f6e0e96a114..4630360c263 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -19,8 +19,8 @@ "A_new_owner_will_be_assigned_automatically_to__count__rooms": "新的所有者將自動分é…給 <span style=\"font-weight: bold;\">__count__</span> 個房間。", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "新所有者將自動分é…給 <span style=\"font-weight: bold;\">__roomName__</span> 房間。", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "新所有者將自動分é…給這些<span style=\"font-weight: bold;\">__count__</span>個房間:<br/> __rooms__。", - "Accept": "接å—", "Accept_Call": "接å—通話", + "Accept": "接å—", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "在沒有線上客æœæ™‚,ä»æŽ¥å—線上å³æ™‚èŠå¤©çš„請求", "Accept_new_livechats_when_agent_is_idle": "當代ç†é–’置時å…許新的å³æ™‚èŠå¤©éœ€æ±‚", "Accept_with_no_online_agents": "無線上客æœä»æŽ¥å—", @@ -1902,7 +1902,6 @@ "files": "檔案", "Files": "檔案", "Files_only": "åªåˆªé™¤é™„åŠ çš„æ–‡ä»¶ï¼Œä¿ç•™è¨Šæ¯", - "files_pruned": "修剪éŽçš„檔案", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", @@ -2037,6 +2036,7 @@ "Global_purge_override_warning": "制定了全域ä¿ç•™åŽŸå‰‡ã€‚如果ç¦ç”¨â€œè¦†è“‹å…¨åŸŸä¿ç•™åŽŸå‰‡â€ï¼Œå‰‡åªèƒ½æ‡‰ç”¨æ¯”å…¨åŸŸåŽŸå‰‡æ›´åš´æ ¼çš„åŽŸå‰‡ã€‚", "Global_Search": "全域æœå°‹", "Go_to_your_workspace": "轉到您的工作å€", + "Hold_Call": "ä¿ç•™é€šè©±", "GoogleCloudStorage": "Google 雲端儲å˜", "GoogleNaturalLanguage_ServiceAccount_Description": "æœå‹™å¸³è™Ÿé‡‘é‘° JSON 文件。更多訊æ¯å¯ä»¥åœ¨[這裡]找到(https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Google æ¨™ç±¤ç®¡ç† ID", @@ -2080,7 +2080,6 @@ "Highlights_List": "強調的å—", "History": "æ·å²", "Hold_Time": "ä¿ç•™æ™‚é–“", - "Hold_Call": "ä¿ç•™é€šè©±", "Home": "首é ", "Host": "主機", "Hospitality_Businness": "招待æ¥å‹™", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 0d8fc4a2ff7..d4357d6340a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -1729,7 +1729,6 @@ "files": "文件", "Files": "文件", "Files_only": "åªåˆ é™¤é™„åŠ çš„æ–‡ä»¶ï¼Œä¿ç•™æ¶ˆæ¯", - "files_pruned": "修剪过的文件", "FileSize_Bytes": "__fileSize__ Bytes", "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", -- GitLab From aefa44b71acc7cbb3b77781f19225407b2ebb6e2 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Mon, 26 Sep 2022 23:45:31 +0530 Subject: [PATCH 077/107] Chore: Missing Also_send_thread_message_to_channel setting translation (#26926) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- apps/meteor/app/lib/server/startup/settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 7c60d8c9c02..45051f6ec69 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -443,6 +443,7 @@ settingsRegistry.addGroup('Accounts', function () { }, ], public: true, + i18nLabel: 'Also_send_thread_message_to_channel_behavior', }); this.add('Accounts_Default_User_Preferences_sidebarShowFavorites', true, { -- GitLab From 6287004d19676d29214c5efec1b660dc6968b90e Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Mon, 26 Sep 2022 15:24:31 -0600 Subject: [PATCH 078/107] Chore: Tests for EE features (#26939) --- .../app/api/server/helpers/requestParams.ts | 2 +- .../tests/data/livechat/canned-responses.ts | 24 ++ apps/meteor/tests/data/livechat/rooms.ts | 17 ++ apps/meteor/tests/data/livechat/units.ts | 48 +++ .../tests/end-to-end/api/livechat/14-units.ts | 280 ++++++++++++++++++ .../api/livechat/15-canned-responses.ts | 150 ++++++++++ .../end-to-end/api/livechat/16-video-call.ts | 125 ++++++++ 7 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/tests/data/livechat/canned-responses.ts create mode 100644 apps/meteor/tests/data/livechat/units.ts create mode 100644 apps/meteor/tests/end-to-end/api/livechat/14-units.ts create mode 100644 apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts create mode 100644 apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts diff --git a/apps/meteor/app/api/server/helpers/requestParams.ts b/apps/meteor/app/api/server/helpers/requestParams.ts index a68205ab09e..a7f11d5ffec 100644 --- a/apps/meteor/app/api/server/helpers/requestParams.ts +++ b/apps/meteor/app/api/server/helpers/requestParams.ts @@ -1,5 +1,5 @@ import { API } from '../api'; API.helperMethods.set('requestParams', function _requestParams(this: any) { - return ['POST', 'PUT'].includes(this.request.method) ? this.bodyParams : this.queryParams; + return ['POST', 'PUT', 'DELETE'].includes(this.request.method) ? this.bodyParams : this.queryParams; }); diff --git a/apps/meteor/tests/data/livechat/canned-responses.ts b/apps/meteor/tests/data/livechat/canned-responses.ts new file mode 100644 index 00000000000..c177935aae4 --- /dev/null +++ b/apps/meteor/tests/data/livechat/canned-responses.ts @@ -0,0 +1,24 @@ +import faker from '@faker-js/faker'; +import { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; +import { api, credentials, request } from '../api-data'; +import type { DummyResponse } from './utils'; + +export const createCannedResponse = (): Promise<Partial<IOmnichannelCannedResponse>> => + new Promise((resolve, reject) => { + const response = { + shortcut: `${faker.random.word()}-${Date.now()}`, + scope: 'user', + tags: [faker.random.word()], + text: faker.lorem.sentence(), + }; + return request + .post(api(`canned-responses`)) + .set(credentials) + .send(response) + .end((_err: Error, _res: DummyResponse<boolean>) => { + if (_err) { + return reject(_err); + } + resolve(response); + }); + }); diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 6a51e596db9..8da85c036b3 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -187,3 +187,20 @@ export const sendMessage = (roomId: string, message: string, visitorToken: strin }); }); } + +export const fetchMessages = (roomId: string, visitorToken: string): Promise<IMessage[]> => { + return new Promise((resolve, reject) => { + request + .get(api(`livechat/messages.history/${roomId}`)) + .set(credentials) + .query({ + token: visitorToken, + }) + .end((err: Error, res: DummyResponse<IMessage[]>) => { + if (err) { + return reject(err); + } + resolve(res.body.messages); + }); + }); +} \ No newline at end of file diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts new file mode 100644 index 00000000000..70ab9eed886 --- /dev/null +++ b/apps/meteor/tests/data/livechat/units.ts @@ -0,0 +1,48 @@ +import faker from "@faker-js/faker"; +import { IOmnichannelBusinessUnit } from "@rocket.chat/core-typings"; +import { methodCall, credentials, request } from "../api-data"; +import { DummyResponse } from "./utils"; + +export const createMonitor = async (username: string): Promise<{ _id: string; username: string }> => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:addMonitor`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:addMonitor', + params: [username], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse<string, 'wrapped'>) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; + +export const createUnit = async (monitorId: string, username: string, departmentId: string): Promise<IOmnichannelBusinessUnit> => { + return new Promise((resolve, reject) => { + request + .post(methodCall(`livechat:saveUnit`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveUnit', + params: [null, { name: faker.name.firstName(), visibility: faker.helpers.arrayElement(['public', 'private']) }, [{ monitorId, username }], [{ departmentId }]], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse<string, 'wrapped'>) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; \ No newline at end of file diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts new file mode 100644 index 00000000000..76bbe33001c --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts @@ -0,0 +1,280 @@ +/* eslint-env mocha */ + +import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { createDepartment } from '../../../data/livechat/rooms'; +import { createMonitor, createUnit } from '../../../data/livechat/units'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { createUser } from '../../../data/users.helper'; +import { IS_EE } from '../../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', function () { + this.retries(0); + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true).then(done); + }); + + describe('[GET] livechat/units', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request + .get(api('livechat/units')) + .set(credentials) + .send({ + unitData: { + name: 'test', + enabled: true, + }, + unitMonitors: [], + unitDepartments: [], + }) + .expect(403); + }); + it('should return a list of units', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request.get(api('livechat/units')).set(credentials).expect(200); + expect(body.units).to.be.an('array').with.lengthOf.greaterThan(0); + const unitFound = body.units.find((u: IOmnichannelBusinessUnit) => u._id === unit._id); + expect(unitFound).to.have.property('_id', unit._id); + expect(unitFound).to.have.property('name', unit.name); + expect(unitFound).to.have.property('numMonitors', 1); + expect(unitFound).to.have.property('numDepartments', 1); + expect(unitFound).to.have.property('type', 'u'); + }); + }); + + describe('[POST] livechat/units', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request + .post(api('livechat/units')) + .set(credentials) + .send({ + unitData: { + name: 'test', + enabled: true, + }, + unitMonitors: [], + unitDepartments: [], + }) + .expect(403); + }); + it('should return a created unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + + const { body } = await request + .post(api('livechat/units')) + .set(credentials) + .send({ + unitData: { name: 'test', visibility: 'public', enabled: true, description: 'test' }, + unitMonitors: [{ monitorId: user._id, username: user.username }], + unitDepartments: [{ departmentId: department._id }], + }) + .expect(200); + + expect(body).to.have.property('_id'); + expect(body).to.have.property('name', 'test'); + expect(body).to.have.property('visibility', 'public'); + expect(body).to.have.property('type', 'u'); + expect(body).to.have.property('numMonitors', 1); + expect(body).to.have.property('numDepartments', 1); + }); + }); + + describe('[GET] livechat/units/:id', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request.get(api('livechat/units/123')).set(credentials).send().expect(403); + }); + it('should return a unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .get(api(`livechat/units/${unit._id}`)) + .set(credentials) + .expect(200); + expect(body).to.have.property('_id', unit._id); + expect(body).to.have.property('name', unit.name); + expect(body).to.have.property('numMonitors', 1); + expect(body).to.have.property('numDepartments', 1); + expect(body).to.have.property('type', 'u'); + }); + }); + + describe('[POST] livechat/units/:id', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request.post(api('livechat/units/123')).set(credentials).expect(403); + }); + it('should return a updated unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .post(api(`livechat/units/${unit._id}`)) + .set(credentials) + .send({ + unitData: { name: 'test', visibility: 'private', enabled: true, description: 'test' }, + unitMonitors: [{ monitorId: user._id, username: user.username }], + unitDepartments: [{ departmentId: department._id }], + }) + .expect(200); + + expect(body).to.have.property('_id'); + expect(body).to.have.property('name', 'test'); + expect(body).to.have.property('visibility', 'private'); + expect(body).to.have.property('type', 'u'); + expect(body).to.have.property('numMonitors', 1); + expect(body).to.have.property('numDepartments', 1); + }); + }); + + describe('[DELETE] livechat/units/:id', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request.delete(api('livechat/units/123')).set(credentials).expect(403); + }); + it('should return a deleted unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .delete(api(`livechat/units/${unit._id}`)) + .set(credentials) + .expect(200); + + expect(body).to.have.be.a('number').equal(1); + }); + }); + + describe('livechat/units/:unitId/departments', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request.get(api('livechat/units/123/departments')).set(credentials).expect(403); + }); + it('should return departments associated with a unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .get(api(`livechat/units/${unit._id}/departments`)) + .set(credentials) + .expect(200); + + expect(body).to.have.property('departments'); + expect(body.departments).to.have.lengthOf(1); + expect(body.departments[0]).to.have.property('_id', department._id); + expect(body.departments[0]).to.have.property('name', department.name); + }); + }); + + describe('livechat/units/:unitId/departments/available', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-units', []); + return request.get(api('livechat/units/123/departments/available')).set(credentials).expect(403); + }); + it('should return departments not associated with a unit', async () => { + await updatePermission('manage-livechat-units', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .get(api(`livechat/units/${unit._id}/departments/available`)) + .set(credentials) + .expect(200); + + expect(body).to.have.property('departments'); + expect(body.departments).to.have.lengthOf.greaterThan(0); + + const myUnit = body.departments.find((d: ILivechatDepartment) => d.parentId === unit._id); + expect(myUnit).to.not.be.undefined.and.not.be.null; + }); + }); + + describe('livechat/units/:unitId/monitors', () => { + it('should fail if manage-livechat-units permission is missing', async () => { + await updatePermission('manage-livechat-monitors', []); + return request.get(api('livechat/units/123/monitors')).set(credentials).expect(403); + }); + it('should return monitors associated with a unit', async () => { + await updatePermission('manage-livechat-monitors', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + const department = await createDepartment(); + const unit = await createUnit(user._id, user.username, department._id); + + const { body } = await request + .get(api(`livechat/units/${unit._id}/monitors`)) + .set(credentials) + .expect(200); + + expect(body).to.have.property('monitors'); + expect(body.monitors).to.have.lengthOf(1); + expect(body.monitors[0]).to.have.property('monitorId', user._id); + expect(body.monitors[0]).to.have.property('username', user.username); + }); + }); + + describe('livechat/monitors', () => { + it('should fail if manage-livechat-monitors permission is missing', async () => { + await updatePermission('manage-livechat-monitors', []); + return request.get(api('livechat/monitors')).set(credentials).expect(403); + }); + it('should return all monitors', async () => { + await updatePermission('manage-livechat-monitors', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + + const { body } = await request.get(api('livechat/monitors')).set(credentials).query({ text: user.username }).expect(200); + expect(body).to.have.property('monitors'); + expect(body.monitors).to.have.lengthOf(1); + expect(body.monitors[0]).to.have.property('username', user.username); + }); + }); + + describe('livechat/monitors/:username', () => { + it('should fail if manage-livechat-monitors permission is missing', async () => { + await updatePermission('manage-livechat-monitors', []); + return request.get(api('livechat/monitors/123')).set(credentials).expect(403); + }); + it('should return a monitor', async () => { + await updatePermission('manage-livechat-monitors', ['admin']); + const user = await createUser(); + await createMonitor(user.username); + + const { body } = await request + .get(api(`livechat/monitors/${user.username}`)) + .set(credentials) + .expect(200); + expect(body).to.have.property('username', user.username); + }); + }); +}); diff --git a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts new file mode 100644 index 00000000000..1f3b49e1c21 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts @@ -0,0 +1,150 @@ +/* eslint-env mocha */ + +import { expect } from 'chai'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { createCannedResponse } from '../../../data/livechat/canned-responses'; +import { IS_EE } from '../../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Canned responses', function () { + this.retries(0); + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true).then(done); + }); + + describe('canned-responses.get', () => { + it('should throw unauthorized when view-canned-responses permission is not set', async () => { + await updatePermission('view-canned-responses', []); + return request.get(api('canned-responses.get')).set(credentials).expect(403); + }); + it('should return an array of canned responses when available', async () => { + await updatePermission('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + await createCannedResponse(); + const { body } = await request.get(api('canned-responses.get')).set(credentials).expect(200); + expect(body).to.have.property('success', true); + expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0); + expect(body.responses[0]).to.have.property('_id'); + expect(body.responses[0]).to.have.property('shortcut'); + expect(body.responses[0]).to.have.property('scope'); + expect(body.responses[0]).to.have.property('tags'); + expect(body.responses[0]).to.have.property('text'); + expect(body.responses[0]).to.have.property('userId'); + }); + }); + + describe('[GET] canned-responses', () => { + it('should fail if user dont have view-canned-responses permission', async () => { + await updatePermission('view-canned-responses', []); + return request.get(api('canned-responses')).set(credentials).expect(403); + }); + it('should return an array of canned responses when available', async () => { + await updatePermission('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + await createCannedResponse(); + const { body } = await request.get(api('canned-responses')).set(credentials).expect(200); + expect(body).to.have.property('success', true); + expect(body.cannedResponses).to.be.an('array').with.lengthOf.greaterThan(0); + expect(body.cannedResponses[0]).to.have.property('_id'); + expect(body.cannedResponses[0]).to.have.property('shortcut'); + expect(body.cannedResponses[0]).to.have.property('scope'); + expect(body.cannedResponses[0]).to.have.property('tags'); + expect(body.cannedResponses[0]).to.have.property('text'); + expect(body.cannedResponses[0]).to.have.property('userId'); + }); + it('should return a canned response matching the params provided (shortcut)', async () => { + const response = await createCannedResponse(); + const { body } = await request.get(api('canned-responses')).set(credentials).query({ shortcut: response.shortcut }).expect(200); + expect(body).to.have.property('success', true); + expect(body.cannedResponses).to.be.an('array').with.lengthOf(1); + expect(body.cannedResponses[0]).to.have.property('_id'); + expect(body.cannedResponses[0]).to.have.property('shortcut', response.shortcut); + }); + it('should return a canned response matching the params provided (scope)', async () => { + const response = await createCannedResponse(); + const { body } = await request.get(api('canned-responses')).set(credentials).query({ scope: response.scope }).expect(200); + expect(body).to.have.property('success', true); + expect(body.cannedResponses).to.be.an('array').with.lengthOf.greaterThan(0); + expect(body.cannedResponses[0]).to.have.property('_id'); + expect(body.cannedResponses[0]).to.have.property('scope', response.scope); + }); + it('should return a canned response matching the params provided (tags)', async () => { + const response = await createCannedResponse(); + const { body } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': response.tags[0] }).expect(200); + expect(body).to.have.property('success', true); + expect(body.cannedResponses).to.be.an('array').with.lengthOf(1); + expect(body.cannedResponses[0]).to.have.property('_id'); + expect(body.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(response.tags[0]); + }); + it('should return a canned response matching the params provided (text)', async () => { + const response = await createCannedResponse(); + const { body } = await request.get(api('canned-responses')).set(credentials).query({ text: response.text }).expect(200); + expect(body).to.have.property('success', true); + expect(body.cannedResponses).to.be.an('array').with.lengthOf(1); + expect(body.cannedResponses[0]).to.have.property('_id'); + expect(body.cannedResponses[0]).to.have.property('text', response.text); + }); + }); + + describe('[POST] canned-responses', () => { + it('should fail if user dont have save-canned-responses permission', async () => { + await updatePermission('save-canned-responses', []); + return request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcut', scope: 'user', tags: ['tag'], text: 'text' }) + .expect(403); + }); + it('should fail if shortcut is not on the request', async () => { + await updatePermission('save-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + return request.post(api('canned-responses')).set(credentials).expect(400); + }); + it('should fail if text is not on the request', async () => { + return request.post(api('canned-responses')).set(credentials).send({ shortcut: 'shortcut' }).expect(400); + }); + it('should fail if scope is not on the request', async () => { + return request.post(api('canned-responses')).set(credentials).send({ shortcut: 'shortcut', text: 'text' }).expect(400); + }); + it('should fail if tags is not an array of strings', async () => { + return request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcut', text: 'text', scope: 'department', tags: 'tag' }) + .expect(400); + }); + it('should create a new canned response', async () => { + const { body } = await request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .expect(200); + expect(body).to.have.property('success', true); + }); + it('should fail if shortcut is already in use', async () => { + return request + .post(api('canned-responses')) + .set(credentials) + .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .expect(400); + }); + }); + + describe('[DELETE] canned-responses', () => { + it('should fail if user dont have remove-canned-responses permission', async () => { + await updatePermission('remove-canned-responses', []); + return request.delete(api('canned-responses')).send({ _id: 'sfdads' }).set(credentials).expect(403); + }); + it('should fail if _id is not on the request', async () => { + await updatePermission('remove-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + return request.delete(api('canned-responses')).set(credentials).expect(400); + }); + it('should delete a canned response', async () => { + const response = await createCannedResponse(); + const { body: cr } = await request.get(api('canned-responses')).set(credentials).query({ shortcut: response.shortcut }).expect(200); + const { body } = await request.delete(api('canned-responses')).send({ _id: cr.cannedResponses[0]._id }).set(credentials).expect(200); + expect(body).to.have.property('success', true); + }); + }); +}); diff --git a/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts b/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts new file mode 100644 index 00000000000..0445804dd08 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts @@ -0,0 +1,125 @@ +/* eslint-env mocha */ + +import { expect } from 'chai'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { createLivechatRoom, createVisitor, fetchMessages, sendMessage } from '../../../data/livechat/rooms'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; + +describe('LIVECHAT - WebRTC video call', function () { + this.retries(0); + + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true) + .then(() => updateSetting('Livechat_accept_chats_with_no_agents', true)) + .then(() => done()); + }); + + describe('livechat/webrtc.call', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + const response = await request.get(api('livechat/webrtc.call')).set(credentials).query({ + rid: 'invalid-room', + }); + expect(response.statusCode).to.be.equal(403); + await updatePermission('view-l-room', ['user', 'bot', 'livechat-agent', 'admin']); + }); + it('should fail if room doesnt exists', async () => { + const response = await request.get(api('livechat/webrtc.call')).set(credentials).query({ + rid: 'invalid-room', + }); + expect(response.statusCode).to.be.equal(400); + }); + it('should fail if WebRTC_Enabled setting is set to false', async () => { + await updateSetting('WebRTC_Enabled', false); + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const response = await request.get(api('livechat/webrtc.call')).set(credentials).query({ + rid: room._id, + }); + expect(response.statusCode).to.be.equal(400); + await updateSetting('WebRTC_Enabled', true); + }); + it('should fail if WebRTC_Enabled is true but Omnichannel_call_provider setting is not WebRTC', async () => { + await updateSetting('Omnichannel_call_provider', 'Jitsi'); + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const response = await request.get(api('livechat/webrtc.call')).set(credentials).query({ + rid: room._id, + }); + expect(response.statusCode).to.be.equal(400); + await updateSetting('Omnichannel_call_provider', 'WebRTC'); + }); + it('should return callStatus as "ringing" when room doesnt have any active call', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const response = await request.get(api('livechat/webrtc.call')).set(credentials).query({ + rid: room._id, + }); + expect(response.statusCode).to.be.equal(200); + expect(response.body).to.have.a.property('videoCall').that.is.an('object'); + expect(response.body.videoCall).to.have.property('callStatus', 'ringing'); + expect(response.body.videoCall).to.have.property('provider', 'webrtc'); + }); + }); + describe('livechat/webrtc.call/:callId', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + const response = await request + .put(api('livechat/webrtc.call/invalid-call')) + .send({ rid: 'fasd', status: 'invalid' }) + .set(credentials); + expect(response.statusCode).to.be.equal(403); + await updatePermission('view-l-room', ['user', 'bot', 'livechat-agent', 'admin']); + }); + it('should fail if room doesnt exists', async () => { + const response = await request + .put(api('livechat/webrtc.call/invalid-call')) + .send({ rid: 'invalid', status: 'invalid' }) + .set(credentials); + expect(response.statusCode).to.be.equal(400); + }); + it('should fail when rid is good, but callId is invalid', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const response = await request + .put(api('livechat/webrtc.call/invalid-call')) + .send({ rid: room._id, status: 'invalid' }) + .set(credentials); + expect(response.statusCode).to.be.equal(400); + }); + it('should fail when callId points to a message but message is not of type livechat_webrtc_video_call', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + const message = await sendMessage(room._id, 'This is a test message', visitor.token); + const response = await request + .put(api(`livechat/webrtc.call/${message._id}`)) + .send({ rid: room._id, status: 'invalid' }) + .set(credentials); + expect(response.statusCode).to.be.equal(400); + }); + it('should update the call status when all params and room are good', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + // Start call + await request + .get(api('livechat/webrtc.call')) + .set(credentials) + .query({ + rid: room._id, + }) + .expect(200); + const messages = await fetchMessages(room._id, visitor.token); + const callMessage = messages.find((message) => message.t === 'livechat_webrtc_video_call'); + expect(callMessage).to.be.an('object'); + const response = await request + .put(api(`livechat/webrtc.call/${callMessage?._id}`)) + .send({ rid: room._id, status: 'invalid' }) + .set(credentials); + expect(response.statusCode).to.be.equal(200); + expect(response.body).to.have.a.property('status', 'invalid'); + }); + }); +}); -- GitLab From 74cd22b03ac0d4ee44dd91f39b22e55b0e53dc7b Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 26 Sep 2022 21:21:05 -0300 Subject: [PATCH 079/107] Regression: Sidebar Room List not working properly. (#26950) --- .../client/models/CachedCollection.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js index 00750726c84..d6c3f35dd96 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js @@ -228,13 +228,13 @@ export class CachedCollection extends Emitter { const data = await call(this.methodName); this.log(`${data.length} records loaded from server`); data.forEach((record) => { - callbacks.run(`cachedCollection-loadFromServer-${this.name}`, record, 'changed'); - this.collection.direct.upsert({ _id: record._id }, omit(record, '_id')); + const newRecord = callbacks.run(`cachedCollection-loadFromServer-${this.name}`, record, 'changed'); + this.collection.direct.upsert({ _id: newRecord._id }, omit(newRecord, '_id')); - this.onSyncData('changed', record); + this.onSyncData('changed', newRecord); - if (record._updatedAt && record._updatedAt > this.updatedAt) { - this.updatedAt = record._updatedAt; + if (newRecord._updatedAt && newRecord._updatedAt > this.updatedAt) { + this.updatedAt = newRecord._updatedAt; } }); this.updatedAt = this.updatedAt === lastTime ? startTime : this.updatedAt; @@ -278,22 +278,22 @@ export class CachedCollection extends Emitter { const { ChatRoom, CachedChatRoom } = await import('../../../models'); Notifications[eventType || this.eventType](eventName || this.eventName, (t, record) => { this.log('record received', t, record); - callbacks.run(`cachedCollection-received-${this.name}`, record, t); + const newRecord = callbacks.run(`cachedCollection-received-${this.name}`, record, t); if (t === 'removed') { let room; if (this.eventName === 'subscriptions-changed') { - room = ChatRoom.findOne(record.rid); + room = ChatRoom.findOne(newRecord.rid); if (room) { this.removeRoomFromCacheWhenUserLeaves(room._id, ChatRoom, CachedChatRoom); } } else { room = this.collection.findOne({ - _id: record._id, + _id: newRecord._id, }); } - this.collection.remove(record._id); + this.collection.remove(newRecord._id); } else { - const { _id, ...recordData } = record; + const { _id, ...recordData } = newRecord; this.collection.direct.upsert({ _id }, recordData); } this.save(); @@ -351,10 +351,10 @@ export class CachedCollection extends Emitter { for (const record of changes) { const action = record._deletedAt ? 'removed' : 'changed'; - callbacks.run(`cachedCollection-sync-${this.name}`, record, action); - const actionTime = record._deletedAt || record._updatedAt; - const { _id, ...recordData } = record; - if (record._deletedAt) { + const newRecord = callbacks.run(`cachedCollection-sync-${this.name}`, record, action); + const actionTime = newRecord._deletedAt || newRecord._updatedAt; + const { _id, ...recordData } = newRecord; + if (newRecord._deletedAt) { this.collection.direct.remove({ _id }); } else { this.collection.direct.upsert({ _id }, recordData); @@ -362,7 +362,7 @@ export class CachedCollection extends Emitter { if (actionTime > this.updatedAt) { this.updatedAt = actionTime; } - this.onSyncData(action, record); + this.onSyncData(action, newRecord); } this.updatedAt = this.updatedAt === lastTime ? startTime : this.updatedAt; -- GitLab From 3a2b6da0e1ae1bba6aec17a03d4ad78f66a285a4 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Tue, 27 Sep 2022 15:51:44 -0300 Subject: [PATCH 080/107] [FIX] Adjusted livechat fallbacks to take null values into account (#26909) * [FIX] Adjusted livechat fallbacks to take null values into account * [FIX] Adjusted to clear defaultAgent when department is changed * [FIX] Fixed default guest info not being displayed * [FIX] Adjusted widget default values logic * [FIX] Removing unnecessary check * [FIX] Changed default values logic again to account for changes via api Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- packages/livechat/src/components/App/App.js | 8 ++++- packages/livechat/src/lib/hooks.js | 13 +++++-- packages/livechat/src/lib/room.js | 3 +- packages/livechat/src/lib/triggers.js | 2 +- .../livechat/src/routes/Chat/container.js | 3 +- .../livechat/src/routes/Register/component.js | 34 +++++++------------ 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/livechat/src/components/App/App.js b/packages/livechat/src/components/App/App.js index 91accd921c4..c774165050d 100644 --- a/packages/livechat/src/components/App/App.js +++ b/packages/livechat/src/components/App/App.js @@ -205,10 +205,13 @@ export class App extends Component { } } - render = ({ sound, undocked, minimized, expanded, alerts, modal }, { initialized, poppedOut }) => { + render = ({ sound, undocked, minimized, expanded, alerts, modal, iframe }, { initialized, poppedOut }) => { if (!initialized) { return null; } + + const { department, name, email } = iframe.guest || {}; + const screenProps = { notificationsEnabled: sound && sound.enabled, minimized: !poppedOut && (minimized || undocked), @@ -217,6 +220,9 @@ export class App extends Component { sound, alerts, modal, + nameDefault: name, + emailDefault: email, + departmentDefault: department, onEnableNotifications: this.handleEnableNotifications, onDisableNotifications: this.handleDisableNotifications, onMinimize: this.handleMinimize, diff --git a/packages/livechat/src/lib/hooks.js b/packages/livechat/src/lib/hooks.js index 46c2c11fa0f..68f28466918 100644 --- a/packages/livechat/src/lib/hooks.js +++ b/packages/livechat/src/lib/hooks.js @@ -39,7 +39,9 @@ const api = { Triggers.processRequest(info); } - const { token, room: { _id: rid } = {} } = store.state; + const { token, room } = store.state; + const { _id: rid } = room || {}; + const { change, title, @@ -75,14 +77,21 @@ const api = { async setDepartment(value) { const { + user, config: { departments = [] }, - user: { department: existingDepartment } = {}, + defaultAgent, } = store.state; + const { department: existingDepartment } = user || {}; + const department = departments.find((dep) => dep._id === value || dep.name === value)?._id || ''; updateIframeGuestData({ department }); + if (defaultAgent && defaultAgent.department !== department) { + store.setState({ defaultAgent: null }); + } + if (department !== existingDepartment) { await loadConfig(); await loadMessages(); diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index c33d29ce89f..3cfd8918234 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -299,7 +299,8 @@ export const loadMessages = async () => { }; export const loadMoreMessages = async () => { - const { room: { _id: rid } = {}, messages = [], noMoreMessages = false } = store.state; + const { room, messages = [], noMoreMessages = false } = store.state; + const { _id: rid } = room || {}; if (!rid || noMoreMessages) { return; diff --git a/packages/livechat/src/lib/triggers.js b/packages/livechat/src/lib/triggers.js index f366ae1e46b..b1ce9dbb723 100644 --- a/packages/livechat/src/lib/triggers.js +++ b/packages/livechat/src/lib/triggers.js @@ -38,7 +38,7 @@ const getAgent = (triggerAction) => { return reject(error); } - store.setState({ defaultAgent: { ...agent, ts: Date.now() } }); + store.setState({ defaultAgent: { ...agent, department, ts: Date.now() } }); resolve(agent); } else if (params.sender === 'custom') { resolve({ diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js index 112011e0f17..5c2a76ce24d 100644 --- a/packages/livechat/src/routes/Chat/container.js +++ b/packages/livechat/src/routes/Chat/container.js @@ -190,7 +190,8 @@ class ChatContainer extends Component { return; } - const { alerts, dispatch, room: { _id: rid } = {} } = this.props; + const { alerts, dispatch, room } = this.props; + const { _id: rid } = room || {}; await dispatch({ loading: true }); try { diff --git a/packages/livechat/src/routes/Register/component.js b/packages/livechat/src/routes/Register/component.js index 60cb1fda37e..aee08f7b7ce 100644 --- a/packages/livechat/src/routes/Register/component.js +++ b/packages/livechat/src/routes/Register/component.js @@ -126,34 +126,26 @@ class Register extends Component { } static getDerivedStateFromProps(nextProps, state) { - const { hasNameField, hasEmailField, hasDepartmentField, departmentDefault, departments, nameDefault, emailDefault } = nextProps; + const { hasNameField, hasEmailField, hasDepartmentField, nameDefault, emailDefault, departmentDefault } = nextProps; + const { name, email, department } = state; + const newState = {}; - const nameValue = nameDefault || ''; - if (hasNameField && (!state.name || state.name !== nameValue)) { - state = { ...state, name: { ...state.name, value: nameValue } }; - } else if (!hasNameField) { - state = { ...state, name: null }; + if (hasNameField && nameDefault && nameDefault !== name?.value) { + const error = validate(this.props, { name: 'name', value: nameDefault, regexp: name?.regexp }); + newState.name = { ...name, value: nameDefault, error }; } - const emailValue = emailDefault || ''; - if (hasEmailField && (!state.email || state.name !== emailValue)) { - state = { ...state, email: { ...state.email, value: emailValue } }; - } else if (!hasEmailField) { - state = { ...state, email: null }; + if (hasEmailField && emailDefault && emailDefault !== email?.value) { + const error = validate(this.props, { name: 'email', value: emailDefault, regexp: email?.regexp }); + newState.email = { ...email, value: emailDefault, error }; } - const departmentValue = departmentDefault || getDefaultDepartment(departments); - const showDepartmentField = hasDepartmentField && departments && departments.length > 1; - if (showDepartmentField && (!state.department || state.department !== departmentValue)) { - state = { ...state, department: { ...state.department, value: departmentValue } }; - } else if (!showDepartmentField) { - state = { ...state, department: null }; + if (hasDepartmentField && departmentDefault && departmentDefault !== department?.value) { + const error = validate(this.props, { name: 'department', value: departmentDefault, regexp: department?.regexp }); + newState.department = { ...department, value: departmentDefault, error }; } - for (const { fieldName: name, value, regexp } of getValidableFields(state)) { - const error = validate(nextProps, { name, value, regexp }); - state = { ...state, [name]: { ...state[name], value, error, showError: false } }; - } + return newState; } state = { -- GitLab From 73bbe44ca20608eee8f8532e59f093b0fb77a62d Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Tue, 27 Sep 2022 23:31:05 -0300 Subject: [PATCH 081/107] Bump version to 5.2.0-rc.1 --- .github/history.json | 140 +++++++++++++++++++++++++- HISTORY.md | 73 ++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 216 insertions(+), 5 deletions(-) diff --git a/.github/history.json b/.github/history.json index 8a261b327ab..2efade6727d 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94511,6 +94511,144 @@ ] } ] + }, + "5.1.3": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26914", + "title": "Release 5.1.3", + "userLogin": "tassoevan", + "contributors": [ + "filipemarins", + "tassoevan", + "pierre-lehnen-rc", + "debdutdeb" + ] + }, + { + "pr": "26880", + "title": "[FIX] `MongoInvalidArgumentError` on overwriting existing setting", + "userLogin": "debdutdeb", + "milestone": "5.1.3", + "contributors": [ + "debdutdeb", + "sampaiodiego", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26917", + "title": "[FIX] Error when mentioning a non-member of a public channel", + "userLogin": "debdutdeb", + "milestone": "5.1.3", + "contributors": [ + "debdutdeb", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26836", + "title": "[FIX] Importer fails when file includes user without an email.", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.1.3", + "contributors": [ + "pierre-lehnen-rc", + "kodiakhq[bot]", + "web-flow" + ] + }, + { + "pr": "26652", + "title": "[FIX] Check if messsage.replies exist on new message template", + "userLogin": "filipemarins", + "milestone": "5.1.3", + "contributors": [ + "filipemarins", + "web-flow", + "kodiakhq[bot]" + ] + }, + { + "pr": "26822", + "title": "Release 5.1.1", + "userLogin": "tassoevan", + "contributors": [ + "hugocostadev", + "tassoevan", + "MartinSchoeler", + "tiagoevanp" + ] + } + ] + }, + "5.2.0-rc.1": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26909", + "title": "[FIX] Adjusted livechat fallbacks to take null values into account", + "userLogin": "aleksandernsilva", + "milestone": "5.1.4", + "contributors": [ + "aleksandernsilva", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26950", + "title": "Regression: Sidebar Room List not working properly.", + "userLogin": "gabriellsh", + "description": "The CachedCollection Class was assuming that the passed parameter would be modified, instead of using the callback return.", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26939", + "title": "Chore: Tests for EE features", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "26926", + "title": "Chore: Missing Also_send_thread_message_to_channel setting translation", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal", + "web-flow", + "kodiakhq[bot]", + "dougfabris" + ] + }, + { + "pr": "26948", + "title": "i18n: Language update from LingoHub 🤖 on 2022-09-26Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "yash-rajpal" + ] + } + ] } } -} +} \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 3854ac617f8..b878a4ed1a7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,39 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.1 +`2022-09-27 · 1 🛠· 4 🔠· 6 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +### 🛠Bug fixes + + +- Adjusted livechat fallbacks to take null values into account ([#26909](https://github.com/RocketChat/Rocket.Chat/pull/26909)) + +<details> +<summary>🔠Minor changes</summary> + + +- Chore: Missing Also_send_thread_message_to_channel setting translation ([#26926](https://github.com/RocketChat/Rocket.Chat/pull/26926)) + +- Chore: Tests for EE features ([#26939](https://github.com/RocketChat/Rocket.Chat/pull/26939)) + +- i18n: Language update from LingoHub 🤖 on 2022-09-26Z ([#26948](https://github.com/RocketChat/Rocket.Chat/pull/26948)) + +- Regression: Sidebar Room List not working properly. ([#26950](https://github.com/RocketChat/Rocket.Chat/pull/26950)) + + The CachedCollection Class was assuming that the passed parameter would be modified, instead of using the callback return. + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@dougfabris](https://github.com/dougfabris) +- [@gabriellsh](https://github.com/gabriellsh) +- [@tassoevan](https://github.com/tassoevan) +- [@yash-rajpal](https://github.com/yash-rajpal) + ## 5.2.0-rc.0 `2022-09-23 · 5 🎉 · 10 🚀 · 21 🛠· 39 🔠· 28 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` @@ -320,6 +353,46 @@ - [@tiagoevanp](https://github.com/tiagoevanp) - [@yash-rajpal](https://github.com/yash-rajpal) +# 5.1.3 +`2022-09-24 · 4 🛠· 2 🔠· 8 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +### Engine versions +- Node: `14.19.3` +- NPM: `6.14.17` +- MongoDB: `4.2, 4.4, 5.0` + +### 🛠Bug fixes + + +- `MongoInvalidArgumentError` on overwriting existing setting ([#26880](https://github.com/RocketChat/Rocket.Chat/pull/26880)) + +- Check if messsage.replies exist on new message template ([#26652](https://github.com/RocketChat/Rocket.Chat/pull/26652)) + +- Error when mentioning a non-member of a public channel ([#26917](https://github.com/RocketChat/Rocket.Chat/pull/26917)) + +- Importer fails when file includes user without an email. ([#26836](https://github.com/RocketChat/Rocket.Chat/pull/26836)) + +<details> +<summary>🔠Minor changes</summary> + + +- Release 5.1.1 ([#26822](https://github.com/RocketChat/Rocket.Chat/pull/26822)) + +- Release 5.1.3 ([#26914](https://github.com/RocketChat/Rocket.Chat/pull/26914)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@debdutdeb](https://github.com/debdutdeb) +- [@filipemarins](https://github.com/filipemarins) +- [@hugocostadev](https://github.com/hugocostadev) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) + # 5.1.2 `2022-09-12 · 1 🛠· 1 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index f667f4c5465..187625b7155 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.0 +ENV RC_VERSION 5.2.0-rc.1 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 75487158bb6..7894a6b4d28 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.0" + "version": "5.2.0-rc.1" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 1087a5bb473..f7d127dc828 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.0", + "version": "5.2.0-rc.1", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 8b78f3fbcd9..4903263703a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.0", + "version": "5.2.0-rc.1", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From a2595bb7e320d297b1032b4746430164eababc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 28 Sep 2022 16:46:49 -0300 Subject: [PATCH 082/107] [FIX] Admin sidebar navigation (#26958) --- .../meteor/client/components/Sidebar/SidebarNavigationItem.tsx | 3 ++- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx index 5d9127e4b2f..f3fdd805986 100644 --- a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx @@ -25,7 +25,8 @@ const SidebarNavigationItem: FC<SidebarNavigationItemProps> = ({ }) => { const params = useMemo(() => ({ group: pathGroup }), [pathGroup]); const path = useRoutePath(pathSection, params); - const isActive = path === currentPath || false; + const isActive = currentPath?.includes(path as string); + if (permissionGranted === false || (typeof permissionGranted === 'function' && !permissionGranted())) { return null; } diff --git a/yarn.lock b/yarn.lock index fff445850b9..782c95397b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23023,7 +23023,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d + checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 languageName: node linkType: hard -- GitLab From e15b467072c7b74c4b530b1dd37add748d9e1e34 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 29 Sep 2022 15:34:00 -0300 Subject: [PATCH 083/107] Revert "[IMPROVE] VideoConference Messages UI (#26548)" (#26961) This reverts commit 5ed3271f8344f947988e8002a02a939b528ab082. --- .../client/views/blocks/MessageBlock.js | 39 +-- apps/meteor/package.json | 10 +- .../rocketchat-i18n/i18n/en.i18n.json | 4 - .../server/models/raw/VideoConference.ts | 5 +- .../modules/listeners/listeners.module.ts | 6 - apps/meteor/server/sdk/lib/Events.ts | 2 - .../services/video-conference/service.ts | 225 +++++++++++---- ee/apps/ddp-streamer/package.json | 2 +- packages/agenda/package.json | 2 +- packages/api-client/package.json | 2 +- packages/cas-validate/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/core-typings/src/IVideoConference.ts | 2 +- packages/eslint-config/package.json | 2 +- packages/favicon/package.json | 2 +- packages/fuselage-ui-kit/package.json | 27 +- .../VideoConferenceBlock.tsx | 183 ------------ .../hooks/useVideoConfData.ts | 12 - .../hooks/useVideoConfDataStream.ts | 63 ----- .../src/blocks/VideoConferenceBlock/index.tsx | 3 - .../src/contexts/kitContext.ts | 1 - .../src/surfaces/FuselageSurfaceRenderer.tsx | 26 +- .../src/surfaces/MessageSurfaceRenderer.tsx | 41 --- .../fuselage-ui-kit/src/surfaces/index.ts | 3 +- packages/gazzodown/package.json | 17 +- packages/livechat/package.json | 5 +- .../components/Messages/MessageList/index.js | 6 +- packages/livechat/src/lib/room.js | 4 +- packages/model-typings/package.json | 2 +- .../src/models/IVideoConferenceModel.ts | 2 +- packages/models/package.json | 2 +- packages/node-poplib/package.json | 2 +- packages/rest-typings/package.json | 2 +- packages/ui-client/package.json | 8 +- packages/ui-contexts/package.json | 5 +- packages/ui-video-conf/.eslintrc.json | 13 +- packages/ui-video-conf/.storybook/main.js | 12 - packages/ui-video-conf/.storybook/preview.js | 25 -- packages/ui-video-conf/package.json | 20 +- .../VideoConfMessage.stories.tsx | 110 -------- .../src/VideoConfMessage/VideoConfMessage.tsx | 8 - .../VideoConfMessageAction.tsx | 15 - .../VideoConfMessageFooter.tsx | 14 - .../VideoConfMessageFooterText.tsx | 9 - .../VideoConfMessage/VideoConfMessageIcon.tsx | 39 --- .../VideoConfMessage/VideoConfMessageRow.tsx | 6 - .../VideoConfMessageSkeleton.tsx | 18 -- .../VideoConfMessage/VideoConfMessageText.tsx | 6 - .../VideoConfMessageUserStack.tsx | 16 -- .../src/VideoConfMessage/index.ts | 21 -- .../src/VideoConfPopup/VideoConfPopup.tsx | 2 +- packages/ui-video-conf/src/index.ts | 1 - yarn.lock | 261 ++++++++++++------ 53 files changed, 416 insertions(+), 901 deletions(-) delete mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx delete mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts delete mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts delete mode 100644 packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx delete mode 100644 packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx delete mode 100644 packages/ui-video-conf/.storybook/main.js delete mode 100644 packages/ui-video-conf/.storybook/preview.js delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx delete mode 100644 packages/ui-video-conf/src/VideoConfMessage/index.ts diff --git a/apps/meteor/client/views/blocks/MessageBlock.js b/apps/meteor/client/views/blocks/MessageBlock.js index 965a0d9b395..1b9c194350c 100644 --- a/apps/meteor/client/views/blocks/MessageBlock.js +++ b/apps/meteor/client/views/blocks/MessageBlock.js @@ -1,21 +1,12 @@ import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket.chat/fuselage-ui-kit'; import React from 'react'; import * as ActionManager from '../../../app/ui-message/client/ActionManager'; import { useBlockRendered } from '../../components/message/hooks/useBlockRendered'; -import { - useVideoConfJoinCall, - useVideoConfSetPreferences, - useVideoConfIsCalling, - useVideoConfIsRinging, - useVideoConfDispatchOutgoing, -} from '../../contexts/VideoConfContext'; -import { VideoConfManager } from '../../lib/VideoConfManager'; +import { useVideoConfJoinCall, useVideoConfSetPreferences } from '../../contexts/VideoConfContext'; import { renderMessageBody } from '../../lib/utils/renderMessageBody'; import './textParsers'; -import { useVideoConfWarning } from '../room/contextualBar/VideoConference/useVideoConfWarning'; // TODO: move this to fuselage-ui-kit itself const mrkdwn = ({ text } = {}) => text && <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />; @@ -25,36 +16,14 @@ function MessageBlock({ mid: _mid, rid, blocks, appId }) { const { ref, className } = useBlockRendered(); const joinCall = useVideoConfJoinCall(); const setPreferences = useVideoConfSetPreferences(); - const isCalling = useVideoConfIsCalling(); - const isRinging = useVideoConfIsRinging(); - const dispatchWarning = useVideoConfWarning(); - const dispatchPopup = useVideoConfDispatchOutgoing(); - - const handleOpenVideoConf = useMutableCallback(async (rid) => { - if (isCalling || isRinging) { - return; - } - - try { - await VideoConfManager.loadCapabilities(); - dispatchPopup({ rid }); - } catch (error) { - dispatchWarning(error.error); - } - }); const context = { action: ({ actionId, value, blockId, mid = _mid, appId }, event) => { - if (appId === 'videoconf-core') { + if (appId === 'videoconf-core' && actionId === 'join') { event.preventDefault(); setPreferences({ mic: true, cam: false }); - if (actionId === 'join') { - return joinCall(blockId); - } - - if (actionId === 'callBack') { - return handleOpenVideoConf(blockId); - } + joinCall(blockId); + return; } ActionManager.triggerBlockAction({ diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cd26881d8dc..0d50c3493ab 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -60,11 +60,11 @@ "email": "support@rocket.chat" }, "devDependencies": { - "@babel/core": "^7.18.13", + "@babel/core": "^7.18.9", "@babel/eslint-parser": "^7.18.9", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/preset-env": "^7.18.10", + "@babel/preset-env": "^7.18.9", "@babel/preset-react": "^7.18.6", "@babel/register": "^7.18.9", "@faker-js/faker": "^6.3.1", @@ -126,7 +126,7 @@ "@types/prometheus-gc-stats": "^0.6.2", "@types/proxyquire": "^1.3.28", "@types/psl": "^1.1.0", - "@types/react": "~17.0.48", + "@types/react": "~17.0.47", "@types/react-dom": "~17.0.17", "@types/rewire": "^2.5.28", "@types/semver": "^7.3.10", @@ -154,7 +154,7 @@ "chai-spies": "^1.0.0", "cross-env": "^7.0.3", "emojione-assets": "^4.5.0", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", @@ -212,7 +212,7 @@ "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/fuselage-ui-kit": "workspace:^", + "@rocket.chat/fuselage-ui-kit": "next", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/icons": "next", "@rocket.chat/layout": "next", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index fd1c2a8de6e..965b5b33848 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -766,7 +766,6 @@ "By_author": "By __author__", "cache_cleared": "Cache cleared", "Call": "Call", - "Call_back": "Call back", "Calling": "Calling", "Call_Center": "Voice Channel", "Call_Center_Description": "Configure Rocket.Chat's voice channels", @@ -783,9 +782,7 @@ "Call_number_enterprise_only": "Call number (Enterprise Edition only)", "call-management": "Call Management", "call-management_description": "Permission to start a meeting", - "Call_ongoing": "Call ongoing", "Call_unavailable_for_federation": "Call is unavailable for Federated rooms", - "Call_was_not_answered": "Call was not answered", "Caller": "Caller", "Caller_Id": "Caller ID", "Cam_on": "Cam On", @@ -5239,7 +5236,6 @@ "VoIP_Toggle": "Enable/Disable VoIP", "Chat_opened_by_visitor": "Chat opened by the visitor", "Wait_activation_warning": "Before you can login, your account must be manually activated by an administrator.", - "Waiting_for_answer": "Waiting for answer", "Waiting_queue": "Waiting queue", "Waiting_queue_message": "Waiting queue message", "Waiting_queue_message_description": "Message that will be displayed to the visitors when they get in the queue", diff --git a/apps/meteor/server/models/raw/VideoConference.ts b/apps/meteor/server/models/raw/VideoConference.ts index 4185ed2da50..96d6cc7f311 100644 --- a/apps/meteor/server/models/raw/VideoConference.ts +++ b/apps/meteor/server/models/raw/VideoConference.ts @@ -189,10 +189,7 @@ export class VideoConferenceRaw extends BaseRaw<VideoConference> implements IVid }); } - public async addUserById( - callId: string, - user: Required<Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'>> & { ts?: Date }, - ): Promise<void> { + public async addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void> { await this.updateOneById(callId, { $addToSet: { users: { diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 0f546ac8d50..75ca7aa7a58 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -130,12 +130,6 @@ export class ListenersModule { notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); }); - service.onEvent('message.update', ({ message }) => { - if (message.rid) { - notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); - } - }); - service.onEvent('watch.subscriptions', ({ clientAction, subscription }) => { if (!subscription.u?._id) { return; diff --git a/apps/meteor/server/sdk/lib/Events.ts b/apps/meteor/server/sdk/lib/Events.ts index 07872ed14fe..6b4336d6d70 100644 --- a/apps/meteor/server/sdk/lib/Events.ts +++ b/apps/meteor/server/sdk/lib/Events.ts @@ -22,7 +22,6 @@ import type { IWebdavAccount, ICustomSound, VoipEventDataSignature, - AtLeast, UserStatus, } from '@rocket.chat/core-typings'; @@ -137,5 +136,4 @@ export type EventSignatures = { 'call.callerhangup'(userId: string, data: { roomId: string }): void; 'watch.pbxevents'(data: { clientAction: ClientAction; data: Partial<IPbxEvent>; id: string }): void; 'connector.statuschanged'(enabled: boolean): void; - 'message.update'(data: { message: AtLeast<IMessage, 'rid'> }): void; }; diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index c11b775b288..0808e685ba5 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -23,7 +23,7 @@ import { isGroupVideoConference, isLivechatVideoConference, } from '@rocket.chat/core-typings'; -import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; +import type { MessageSurfaceLayout, ContextBlock } from '@rocket.chat/ui-kit'; import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; @@ -34,6 +34,7 @@ import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; import { Apps } from '../../../app/apps/server'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { settings } from '../../../app/settings/server'; +import { getURL } from '../../../app/utils/server'; import { videoConfProviders } from '../../lib/videoConfProviders'; import { videoConfTypes } from '../../lib/videoConfTypes'; import { updateCounter } from '../../../app/statistics/server/functions/updateStatsCounter'; @@ -146,7 +147,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } if (call.messages.started) { - this.updateVideoConfMessage(call.messages.started); + const name = + (settings.get<boolean>('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; + const text = TAPi18n.__('video_direct_missed', { username: name }); + await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]); } await VideoConferenceModel.setDataById(callId, { @@ -233,8 +237,8 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf throw new Error('Invalid User'); } - const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>>(userId, { - projection: { username: 1, name: 1, avatarETag: 1 }, + const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name'>>>(userId, { + projection: { username: 1, name: 1 }, }); if (!user) { throw new Error('Invalid User'); @@ -244,7 +248,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf _id: user._id, username: user.username, name: user.name, - avatarETag: user.avatarETag, ts: ts || new Date(), }); } @@ -369,13 +372,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return true; } - private async updateVideoConfMessage(messageId: IMessage['_id']): Promise<void> { - const message = await Messages.findOneById(messageId); - if (message) { - api.broadcast('message.update', { message }); - } - } - private async endCall(callId: VideoConference['_id']): Promise<void> { const call = await this.getUnfiltered(callId); if (!call) { @@ -384,11 +380,12 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.ENDED }); if (call.messages?.started) { - this.updateVideoConfMessage(call.messages.started); + await this.removeJoinButton(call.messages.started); } - if (call.type === 'direct') { - return this.endDirectCall(call); + switch (call.type) { + case 'direct': + return this.endDirectCall(call); } } @@ -399,6 +396,16 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf } await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.EXPIRED }); + if (call.messages?.started) { + return this.removeJoinButton(call.messages.started); + } + } + + private async removeJoinButton(messageId: IMessage['_id']): Promise<void> { + await Messages.removeVideoConfJoinButton(messageId); + + const text = TAPi18n.__('Conference_call_has_ended'); + await Messages.addBlocksById(messageId, [this.buildMessageBlock(text)]); } private async endDirectCall(call: IDirectVideoConference): Promise<void> { @@ -436,21 +443,66 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return videoConfTypes.getTypeForRoom(room, allowRinging); } - private async createMessage(call: VideoConference, createdBy?: IUser, customBlocks?: IMessage['blocks']): Promise<IMessage['_id']> { + private async createMessage( + rid: IRoom['_id'], + providerName: string, + extraData: Partial<IMessage> = {}, + createdBy?: IUser, + ): Promise<IMessage['_id']> { const record = { msg: '', groupable: false, - blocks: customBlocks || [this.buildVideoConfBlock(call._id)], + ...extraData, }; - const room = await Rooms.findOneById(call.rid); - const appId = videoConfProviders.getProviderAppId(call.providerName); + const room = await Rooms.findOneById(rid); + const appId = videoConfProviders.getProviderAppId(providerName); const user = createdBy || (appId && (await Users.findOneByAppId(appId))) || (await Users.findOneById('rocket.cat')); const message = sendMessage(user, record, room, false); return message._id; } + private async createDirectCallMessage(call: IDirectVideoConference, user: IUser): Promise<IMessage['_id']> { + const username = (settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; + const text = TAPi18n.__('video_direct_calling', { + username, + }); + + return this.createMessage( + call.rid, + call.providerName, + { + blocks: [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)], + }, + user, + ); + } + + private async createGroupCallMessage(call: IGroupVideoConference, user: IUser, useAppUser = true): Promise<IMessage['_id']> { + const username = (settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username) || user.username || ''; + const text = TAPi18n.__(useAppUser ? 'video_conference_started_by' : 'video_conference_started', { + conference: call.title || '', + username, + }); + + return this.createMessage( + call.rid, + call.providerName, + { + blocks: [ + this.buildMessageBlock(text), + this.buildJoinButtonBlock(call._id, call.title), + { + type: 'context', + elements: [], + }, + ], + } as Partial<IMessage>, + useAppUser ? undefined : user, + ); + } + private async validateProvider(providerName: string): Promise<void> { const manager = await this.getProviderManager(); const configured = await manager.isFullyConfigured(providerName).catch(() => false); @@ -493,37 +545,35 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf username, }); - return this.createMessage(call, user, [ - this.buildMessageBlock(text), + return this.createMessage( + call.rid, + call.providerName, { - type: 'actions', - appId: 'videoconf-core', - blockId: call._id, - elements: [ + blocks: [ + this.buildMessageBlock(text), { + type: 'actions', appId: 'videoconf-core', blockId: call._id, - actionId: 'joinLivechat', - type: 'button', - text: { - type: 'plain_text', - text: TAPi18n.__('Join_call'), - emoji: true, - }, - url, + elements: [ + { + appId: 'videoconf-core', + blockId: call._id, + actionId: 'joinLivechat', + type: 'button', + text: { + type: 'plain_text', + text: TAPi18n.__('Join_call'), + emoji: true, + }, + url, + }, + ], }, ], }, - ]); - } - - private buildVideoConfBlock(callId: string): MessageSurfaceLayout[number] { - return { - type: 'video_conf', - blockId: callId, - callId, - appId: 'videoconf-core', - }; + user, + ); } private buildMessageBlock(text: string): MessageSurfaceLayout[number] { @@ -537,6 +587,27 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }; } + private buildJoinButtonBlock(callId: string, title = ''): MessageSurfaceLayout[number] { + return { + type: 'actions', + appId: 'videoconf-core', + elements: [ + { + appId: 'videoconf-core', + blockId: callId, + actionId: 'join', + value: title, + type: 'button', + text: { + type: 'plain_text', + text: TAPi18n.__('Join_call'), + emoji: true, + }, + }, + ], + }; + } + private async startDirect( providerName: string, user: IUser, @@ -567,8 +638,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const url = await this.generateNewUrl(call); VideoConferenceModel.setUrlById(callId, url); - const messageId = await this.createMessage(call, user); - + const messageId = await this.createDirectCallMessage(call, user); VideoConferenceModel.setMessageById(callId, 'started', messageId); // After 40 seconds if the status is still "calling", we cancel the call automatically. @@ -630,7 +700,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf call.url = url; - const messageId = await this.createMessage(call, useAppUser ? undefined : user); + const messageId = await this.createGroupCallMessage(call, user, useAppUser); VideoConferenceModel.setMessageById(callId, 'started', messageId); if (call.ringing) { @@ -662,7 +732,6 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const joinUrl = await this.getUrl(call); const messageId = await this.createLivechatMessage(call, user, joinUrl); - await VideoConferenceModel.setMessageById(callId, 'started', messageId); return { @@ -800,7 +869,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private async addUserToCall( call: Optional<VideoConference, 'providerData'>, - { _id, username, name, avatarETag, ts }: AtLeast<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date }, + { _id, username, name, avatarETag, ts }: AtLeast<IUser, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date }, ): Promise<void> { if (call.users.find((user) => user._id === _id)) { return; @@ -808,13 +877,64 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.addUserById(call._id, { _id, username, name, avatarETag, ts }); - if (call.type === 'direct') { - return this.updateDirectCall(call as IDirectVideoConference, _id); + switch (call.type) { + case 'videoconference': + return this.updateGroupCallMessage(call as IGroupVideoConference, { _id, username, name }); + case 'direct': + return this.updateDirectCall(call as IDirectVideoConference, _id); } } private async addAnonymousUser(call: Optional<IGroupVideoConference, 'providerData'>): Promise<void> { await VideoConferenceModel.increaseAnonymousCount(call._id); + + if (!call.messages.started) { + return; + } + + const imageUrl = getURL(`/avatar/@a`, { cdn: false, full: true }); + return this.addAvatarToCallMessage(call.messages.started, imageUrl, TAPi18n.__('Anonymous')); + } + + private async addAvatarToCallMessage(messageId: IMessage['_id'], imageUrl: string, altText: string): Promise<void> { + const message = await Messages.findOneById<Pick<IMessage, '_id' | 'blocks'>>(messageId, { projection: { blocks: 1 } }); + if (!message) { + return; + } + + const blocks = message.blocks || []; + + const avatarsBlock = (blocks.find((block) => block.type === 'context') || { type: 'context', elements: [] }) as ContextBlock; + if (!blocks.includes(avatarsBlock)) { + blocks.push(avatarsBlock); + } + + if (avatarsBlock.elements.find((el) => el.type === 'image' && el.imageUrl === imageUrl)) { + return; + } + + avatarsBlock.elements = [ + ...avatarsBlock.elements, + { + type: 'image', + imageUrl, + altText, + }, + ]; + + await Messages.setBlocksById(message._id, blocks); + } + + private async updateGroupCallMessage( + call: Optional<IGroupVideoConference, 'providerData'>, + user: Pick<IUser, '_id' | 'username' | 'name'>, + ): Promise<void> { + if (!call.messages.started || !user.username) { + return; + } + const imageUrl = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); + + return this.addAvatarToCallMessage(call.messages.started, imageUrl, user.name || user.username); } private async updateDirectCall(call: IDirectVideoConference, newUserId: IUser['_id']): Promise<void> { @@ -835,7 +955,10 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf await VideoConferenceModel.setStatusById(call._id, VideoConferenceStatus.STARTED); if (call.messages.started) { - this.updateVideoConfMessage(call.messages.started); + const username = + (settings.get<boolean>('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || ''; + const text = TAPi18n.__('video_direct_started', { username }); + await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)]); } } } diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 5f429eed551..2cdb260f523 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -47,7 +47,7 @@ "@types/sharp": "^0.30.4", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3", - "eslint": "^8.22.0", + "eslint": "^8.21.0", "pino-pretty": "^7.6.1", "ts-node": "^10.9.1", "typescript": "~4.5.5" diff --git a/packages/agenda/package.json b/packages/agenda/package.json index e2a0613c804..5ca16adef57 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/debug": "^4", "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/api-client/package.json b/packages/api-client/package.json index f9c9b1466a5..98f63afd69a 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@types/jest": "^27.4.1", "@types/strict-uri-encode": "^2.0.0", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "ts-jest": "^27.1.5", "typescript": "~4.5.5" diff --git a/packages/cas-validate/package.json b/packages/cas-validate/package.json index 22cc27646cd..c8f23b455c7 100644 --- a/packages/cas-validate/package.json +++ b/packages/cas-validate/package.json @@ -5,7 +5,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 27ae9d5549c..7faf5cf4e29 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "mongodb": "^4.3.1", "prettier": "^2.7.1", "typescript": "~4.5.5" diff --git a/packages/core-typings/src/IVideoConference.ts b/packages/core-typings/src/IVideoConference.ts index ce6297c00cd..a4c0b571347 100644 --- a/packages/core-typings/src/IVideoConference.ts +++ b/packages/core-typings/src/IVideoConference.ts @@ -31,7 +31,7 @@ export type LivechatInstructions = { export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type']; -export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> { +export interface IVideoConferenceUser extends Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> { ts: Date; } diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 2e028655e6a..cc850fdaa0b 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -7,7 +7,7 @@ "@types/prettier": "^2.6.3", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-import": "^2.26.0", diff --git a/packages/favicon/package.json b/packages/favicon/package.json index a50c9aca6d8..7dba2d92dad 100644 --- a/packages/favicon/package.json +++ b/packages/favicon/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "devDependencies": { - "eslint": "^8.22.0", + "eslint": "^8.20.0", "typescript": "~4.5.5" }, "scripts": { diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index d11639351bf..b89ffc23b27 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -38,20 +38,13 @@ "bump-next": "bump-next" }, "peerDependencies": { - "@rocket.chat/apps-engine": "*", - "@rocket.chat/eslint-config": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/fuselage-polyfills": "*", "@rocket.chat/icons": "*", - "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-contexts": "*", - "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "*", - "@tanstack/react-query": "*", - "react": "*", - "react-dom": "*" + "react": "^17.0.2", + "react-dom": "^17.0.2" }, "devDependencies": { "@rocket.chat/apps-engine": "~1.30.0", @@ -62,9 +55,6 @@ "@rocket.chat/icons": "next", "@rocket.chat/prettier-config": "next", "@rocket.chat/styled": "next", - "@rocket.chat/ui-contexts": "workspace:^", - "@rocket.chat/ui-kit": "next", - "@rocket.chat/ui-video-conf": "workspace:^", "@storybook/addon-essentials": "~6.5.10", "@storybook/addons": "~6.5.10", "@storybook/builder-webpack5": "~6.5.10", @@ -72,20 +62,23 @@ "@storybook/react": "~6.5.10", "@storybook/source-loader": "~6.5.10", "@storybook/theming": "~6.5.10", - "@tanstack/react-query": "^4.2.1", - "@types/react": "~17.0.48", - "@types/react-dom": "~17.0.17", + "@types/react": "~17.0.39", + "@types/react-dom": "^17.0.11", "babel-loader": "~8.2.3", "cross-env": "^7.0.3", - "eslint": "~8.22.0", + "eslint": "~8.8.0", "lint-staged": "~12.3.3", "normalize.css": "^8.0.1", "npm-run-all": "^4.1.5", "prettier": "~2.5.1", + "react": "^17.0.2", "react-dom": "^17.0.2", "rimraf": "^3.0.2", - "tslib": "^2.3.1", "typescript": "~4.3.5", "webpack": "~5.68.0" + }, + "dependencies": { + "@rocket.chat/ui-kit": "next", + "tslib": "^2.3.1" } } diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx deleted file mode 100644 index d62d613dcd1..00000000000 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import type * as UiKit from '@rocket.chat/ui-kit'; -import { useTranslation, useUserAvatarPath } from '@rocket.chat/ui-contexts'; -import { Avatar } from '@rocket.chat/fuselage'; -import { - VideoConfMessageSkeleton, - VideoConfMessage, - VideoConfMessageRow, - VideoConfMessageIcon, - VideoConfMessageText, - VideoConfMessageFooter, - VideoConfMessageAction, - VideoConfMessageUserStack, - VideoConfMessageFooterText, -} from '@rocket.chat/ui-video-conf'; -import type { MouseEventHandler, ReactElement } from 'react'; -import React, { useContext, memo } from 'react'; - -import { useSurfaceType } from '../../contexts/SurfaceContext'; -import type { BlockProps } from '../../utils/BlockProps'; -import { useVideoConfDataStream } from './hooks/useVideoConfDataStream'; -import { kitContext } from '../..'; - -type VideoConferenceBlockProps = BlockProps<UiKit.VideoConferenceBlock>; - -const MAX_USERS = 6; - -const VideoConferenceBlock = ({ - block, -}: VideoConferenceBlockProps): ReactElement => { - const t = useTranslation(); - const { callId, appId = 'videoconf-core' } = block; - const surfaceType = useSurfaceType(); - - const { action, viewId, rid } = useContext(kitContext); - - if (surfaceType !== 'message') { - return <></>; - } - - if (!callId || !rid) { - return <></>; - } - - const getUserAvatarPath = useUserAvatarPath(); - const result = useVideoConfDataStream({ rid, callId }); - - const joinHandler: MouseEventHandler<HTMLButtonElement> = (e): void => { - action( - { - blockId: block.blockId || '', - appId, - actionId: 'join', - value: block.blockId || '', - viewId, - }, - e - ); - }; - - const callAgainHandler: MouseEventHandler<HTMLButtonElement> = (e): void => { - action( - { - blockId: rid || '', - appId, - actionId: 'callBack', - value: rid || '', - viewId, - }, - e - ); - }; - - if (result.isSuccess) { - const { data } = result; - - if ('endedAt' in data) { - return ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon /> - <VideoConfMessageText>{t('Call_ended')}</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - {data.type === 'direct' && ( - <> - <VideoConfMessageAction onClick={callAgainHandler}> - {t('Call_back')} - </VideoConfMessageAction> - <VideoConfMessageFooterText> - {t('Call_was_not_answered')} - </VideoConfMessageFooterText> - </> - )} - {data.type !== 'direct' && - (data.users.length ? ( - <> - <VideoConfMessageUserStack> - {data.users.map(({ username }, index) => - data.users.length <= MAX_USERS ? ( - <Avatar - size='x28' - key={index} - data-tooltip={username} - url={getUserAvatarPath(username as string)} - /> - ) : ( - <></> - ) - )} - </VideoConfMessageUserStack> - <VideoConfMessageFooterText> - {data.users.length > 6 - ? `+ ${MAX_USERS - data.users.length} ${t('Joined')}` - : t('Joined')} - </VideoConfMessageFooterText> - </> - ) : ( - <VideoConfMessageFooterText> - {t('Call_was_not_answered')} - </VideoConfMessageFooterText> - ))} - </VideoConfMessageFooter> - </VideoConfMessage> - ); - } - - if (data.type === 'direct' && data.status === 0) { - return ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon variant='incoming' /> - <VideoConfMessageText>{t('Calling')}</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageAction primary onClick={joinHandler}> - {t('Join')} - </VideoConfMessageAction> - <VideoConfMessageFooterText> - {t('Waiting_for_answer')} - </VideoConfMessageFooterText> - </VideoConfMessageFooter> - </VideoConfMessage> - ); - } - - return ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon variant='outgoing' /> - <VideoConfMessageText>{t('Call_ongoing')}</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageAction primary onClick={joinHandler}> - {t('Join')} - </VideoConfMessageAction> - <VideoConfMessageUserStack> - {data.users.map(({ username }, index) => - data.users.length <= MAX_USERS ? ( - <Avatar - size='x28' - key={index} - data-tooltip={username} - url={getUserAvatarPath(username as string)} - /> - ) : ( - <></> - ) - )} - </VideoConfMessageUserStack> - <VideoConfMessageFooterText> - {data.users.length > 6 - ? `+ ${MAX_USERS - data.users.length} ${t('Joined')}` - : t('Joined')} - </VideoConfMessageFooterText> - </VideoConfMessageFooter> - </VideoConfMessage> - ); - } - - return <VideoConfMessageSkeleton />; -}; - -export default memo(VideoConferenceBlock); diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts deleted file mode 100644 index ddbc0861698..00000000000 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfData.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; - -export const useVideoConfData = ({ callId }: { callId: string }) => { - const videoConf = useEndpoint('GET', '/v1/video-conference.info'); - - return useQuery(['video-conference', callId], () => videoConf({ callId }), { - staleTime: Infinity, - refetchOnWindowFocus: false, - // refetchOnMount: false, - }); -}; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts deleted file mode 100644 index d6a77b9d044..00000000000 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useStream } from '@rocket.chat/ui-contexts'; -import { Emitter } from '@rocket.chat/emitter'; -import { useCallback, useEffect } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; - -import { useVideoConfData } from './useVideoConfData'; - -const ee = new Emitter<Record<string, void>>(); - -const events = new Map<string, () => void>(); - -const useStreamBySubPath = ( - streamer: ReturnType<typeof useStream>, - subpath: string, - callback: () => void -) => { - useEffect(() => { - if (!ee.has(subpath)) { - events.set( - subpath, - streamer(subpath, () => { - ee.emit(subpath); - }) - ); - } - - return () => { - if (!ee.has(subpath)) { - events.delete(subpath); - } - }; - }, [streamer, subpath, callback]); - - useEffect(() => { - ee.on(subpath, callback); - - return () => { - ee.off(subpath, callback); - }; - }, [callback, subpath]); -}; - -export const useVideoConfDataStream = ({ - rid, - callId, -}: { - rid: string; - callId: string; -}) => { - const queryClient = useQueryClient(); - const subpath = `${rid}/${callId}`; - - const subscribeNotifyRoom = useStream('notify-room'); - - useStreamBySubPath( - subscribeNotifyRoom, - subpath, - useCallback(() => { - queryClient.invalidateQueries(['video-conference', callId]); - }, [subpath]) - ); - return useVideoConfData({ callId }); -}; diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx deleted file mode 100644 index bbd942c7d3f..00000000000 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import VideoConferenceBlock from './VideoConferenceBlock'; - -export default VideoConferenceBlock; diff --git a/packages/fuselage-ui-kit/src/contexts/kitContext.ts b/packages/fuselage-ui-kit/src/contexts/kitContext.ts index 6eebb8cea7a..b0cb4141b16 100644 --- a/packages/fuselage-ui-kit/src/contexts/kitContext.ts +++ b/packages/fuselage-ui-kit/src/contexts/kitContext.ts @@ -23,7 +23,6 @@ type UiKitContext = { errors?: Record<string, string>; values: Record<string, { value: string } | undefined>; viewId?: string; - rid?: string; }; export const defaultContext = { diff --git a/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx index fe2e61ee590..f548f63ac67 100644 --- a/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx +++ b/packages/fuselage-ui-kit/src/surfaces/FuselageSurfaceRenderer.tsx @@ -18,23 +18,17 @@ import OverflowElement from '../elements/OverflowElement'; import PlainTextInputElement from '../elements/PlainTextInputElement'; import StaticSelectElement from '../elements/StaticSelectElement'; -type FuselageSurfaceRendererProps = ConstructorParameters< - typeof UiKit.SurfaceRenderer ->[0]; - export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer<ReactElement> { - public constructor(allowedBlocks?: FuselageSurfaceRendererProps) { - super( - allowedBlocks || [ - 'actions', - 'context', - 'divider', - 'image', - 'input', - 'section', - 'preview', - ] - ); + public constructor() { + super([ + 'actions', + 'context', + 'divider', + 'image', + 'input', + 'section', + 'preview', + ]); } public plain_text( diff --git a/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx deleted file mode 100644 index 6d83d774a9e..00000000000 --- a/packages/fuselage-ui-kit/src/surfaces/MessageSurfaceRenderer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as UiKit from '@rocket.chat/ui-kit'; -import type { ReactElement } from 'react'; -import React from 'react'; - -import VideoConferenceBlock from '../blocks/VideoConferenceBlock/VideoConferenceBlock'; -import { FuselageSurfaceRenderer } from './FuselageSurfaceRenderer'; - -export class FuselageMessageSurfaceRenderer extends FuselageSurfaceRenderer { - public constructor() { - super([ - 'actions', - 'context', - 'divider', - 'image', - 'input', - 'section', - 'preview', - 'video_conf', - ]); - } - - video_conf( - block: UiKit.VideoConferenceBlock, - context: UiKit.BlockContext, - index: number - ): ReactElement | null { - if (context === UiKit.BlockContext.BLOCK) { - return ( - <VideoConferenceBlock - key={index} - block={block} - context={context} - index={index} - surfaceRenderer={this} - /> - ); - } - - return null; - } -} diff --git a/packages/fuselage-ui-kit/src/surfaces/index.ts b/packages/fuselage-ui-kit/src/surfaces/index.ts index 49eff1a923a..dd291834359 100644 --- a/packages/fuselage-ui-kit/src/surfaces/index.ts +++ b/packages/fuselage-ui-kit/src/surfaces/index.ts @@ -3,11 +3,10 @@ import { FuselageSurfaceRenderer } from './FuselageSurfaceRenderer'; import MessageSurface from './MessageSurface'; import ModalSurface from './ModalSurface'; import { createSurfaceRenderer } from './createSurfaceRenderer'; -import { FuselageMessageSurfaceRenderer } from './MessageSurfaceRenderer'; // export const attachmentParser = new FuselageSurfaceRenderer(); export const bannerParser = new FuselageSurfaceRenderer(); -export const messageParser = new FuselageMessageSurfaceRenderer(); +export const messageParser = new FuselageSurfaceRenderer(); export const modalParser = new FuselageSurfaceRenderer(); // export const UiKitAttachment = createSurfaceRenderer(AttachmentSurface, attachmentParser); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 1e5569f1bc2..b1e9ce93dfb 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -3,10 +3,11 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/core": "~7.18.13", + "@babel/core": "^7.18.9", "@mdx-js/react": "^1.6.22", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", + "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/styled": "next", @@ -26,13 +27,13 @@ "@types/babel__core": "^7", "@types/jest": "^27.4.1", "@types/katex": "~0", - "@types/react": "~17.0.48", - "@types/react-dom": "~17.0.17", + "@types/react": "^17.0.47", + "@types/react-dom": "^18", "@types/testing-library__jest-dom": "^5", "@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/parser": "^5.30.7", "babel-loader": "^8.2.5", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -60,16 +61,16 @@ "/dist" ], "peerDependencies": { - "@rocket.chat/core-typings": "*", + "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "katex": "*", - "react": "*" + "react": "~17.0.2" }, "dependencies": { "highlight.js": "^11.5.1", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index bf9f8e56a3c..d87419d4658 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.18.9", - "@babel/preset-env": "^7.18.10", + "@babel/preset-env": "^7.18.9", "@rocket.chat/eslint-config": "^0.4.0", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/logo": "next", @@ -36,6 +36,7 @@ "@storybook/addon-viewport": "~6.5.10", "@storybook/react": "~6.5.10", "@storybook/theming": "~6.5.10", + "@types/react-dom": "^17.0.17", "autoprefixer": "^9.8.8", "babel-loader": "^8.1.0", "babel-plugin-jsx-pragmatic": "^1.0.2", @@ -43,7 +44,7 @@ "css-loader": "^4.3.0", "cssnano": "^4.1.11", "desvg-loader": "^0.1.0", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", diff --git a/packages/livechat/src/components/Messages/MessageList/index.js b/packages/livechat/src/components/Messages/MessageList/index.js index f7ab458c017..7ac2dbf60ab 100644 --- a/packages/livechat/src/components/Messages/MessageList/index.js +++ b/packages/livechat/src/components/Messages/MessageList/index.js @@ -101,9 +101,7 @@ export class MessageList extends MemoizedComponent { isVideoConfMessage(message) { return Boolean( - message.blocks - ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') - ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'), + message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'), ); } @@ -139,7 +137,7 @@ export class MessageList extends MemoizedComponent { } const videoConfJoinBlock = message.blocks - ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') + ?.find(({ appId }) => appId === 'videoconf-core') ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); if (videoConfJoinBlock) { // If the call is not accepted yet, don't render the message. diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index 3cfd8918234..858df335671 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -35,9 +35,7 @@ export const closeChat = async ({ transcriptRequested } = {}) => { }; const getVideoConfMessageData = (message) => - message.blocks - ?.find(({ appId, type }) => appId === 'videoconf-core' && type === 'actions') - ?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); + message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'); const isVideoCallMessage = (message) => { if (message.t === constants.webRTCCallStartedMessageType) { diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 024d295148f..55516d3bb65 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@types/jest": "^27.4.1", "@types/node-rsa": "^1.1.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "ts-jest": "^27.1.5", diff --git a/packages/model-typings/src/models/IVideoConferenceModel.ts b/packages/model-typings/src/models/IVideoConferenceModel.ts index e0a1090eb5c..d1cfbcd919b 100644 --- a/packages/model-typings/src/models/IVideoConferenceModel.ts +++ b/packages/model-typings/src/models/IVideoConferenceModel.ts @@ -54,7 +54,7 @@ export interface IVideoConferenceModel extends IBaseModel<VideoConference> { setProviderDataById(callId: string, providerData: Record<string, any> | undefined): Promise<void>; - addUserById(callId: string, user: Required<Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'>> & { ts?: Date }): Promise<void>; + addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void>; setMessageById(callId: string, messageType: keyof VideoConference['messages'], messageId: string): Promise<void>; diff --git a/packages/models/package.json b/packages/models/package.json index 6c6773d02ce..77846eb6425 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "ts-jest": "^27.1.5", "typescript": "~4.5.5" diff --git a/packages/node-poplib/package.json b/packages/node-poplib/package.json index 792fc9f6f30..eac15f26ef1 100644 --- a/packages/node-poplib/package.json +++ b/packages/node-poplib/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 603c229976c..0b8648c0d62 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "ts-jest": "^27.1.5", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 39aaa9d9cb6..ce17f2da0d9 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/core": "~7.18.13", "@rocket.chat/css-in-js": "next", "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "next", @@ -19,12 +18,10 @@ "@storybook/manager-webpack4": "~6.5.10", "@storybook/react": "~6.5.10", "@storybook/testing-library": "~0.0.13", - "@types/babel__core": "^7", "@types/jest": "^27.4.1", "@types/postcss-url": "^10", - "@types/react": "~17.0.48", - "@types/react-dom": "~17.0.17", - "eslint": "^8.22.0", + "@types/react": "~17.0.47", + "eslint": "^8.20.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -59,7 +56,6 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "*", "react": "~17.0.2" } } diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 7bfd1f95fdc..7858794225d 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -8,10 +8,9 @@ "@rocket.chat/fuselage-hooks": "next", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "^27.4.1", - "@types/react": "~17.0.48", - "@types/react-dom": "~17.0.17", + "@types/react": "^17.0.47", "@types/use-sync-external-store": "^0.0.3", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "jest": "^27.5.1", "mongodb": "^4.3.1", "react": "~17.0.2", diff --git a/packages/ui-video-conf/.eslintrc.json b/packages/ui-video-conf/.eslintrc.json index 07037ccf0bc..1859ff825f6 100644 --- a/packages/ui-video-conf/.eslintrc.json +++ b/packages/ui-video-conf/.eslintrc.json @@ -5,8 +5,7 @@ "@rocket.chat/eslint-config/original", "prettier", "plugin:anti-trojan-source/recommended", - "plugin:react/jsx-runtime", - "plugin:storybook/recommended" + "plugin:react/jsx-runtime" ], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], @@ -64,13 +63,5 @@ "version": "detect" } }, - "ignorePatterns": ["**/dist"], - "overrides": [ - { - "files": ["*.stories.tsx"], - "rules": { - "react/no-multi-comp": "off" - } - } - ] + "ignorePatterns": ["**/dist"] } diff --git a/packages/ui-video-conf/.storybook/main.js b/packages/ui-video-conf/.storybook/main.js deleted file mode 100644 index 4b6ae93751a..00000000000 --- a/packages/ui-video-conf/.storybook/main.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - "stories": [ - "../src/**/*.stories.mdx", - "../src/**/*.stories.@(js|jsx|ts|tsx)" - ], - "addons": [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-interactions" - ], - "framework": "@storybook/react" -} diff --git a/packages/ui-video-conf/.storybook/preview.js b/packages/ui-video-conf/.storybook/preview.js deleted file mode 100644 index abd704f7951..00000000000 --- a/packages/ui-video-conf/.storybook/preview.js +++ /dev/null @@ -1,25 +0,0 @@ -import '../../../apps/meteor/app/theme/client/main.css'; -import 'highlight.js/styles/github.css'; - -export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -} - -export const decorators = [ - (Story) => ( - <div className="rc-old"> - <style>{` - body { - background-color: white; - } - `}</style> - <Story /> - </div> - ) -]; diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 4603e2340ab..7bab37906fa 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -3,26 +3,15 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/core": "~7.18.13", "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "next", "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/icons": "next", "@rocket.chat/styled": "next", - "@storybook/addon-actions": "~6.5.10", - "@storybook/addon-docs": "~6.5.10", - "@storybook/addon-essentials": "~6.5.10", - "@storybook/builder-webpack4": "~6.5.10", - "@storybook/manager-webpack4": "~6.5.10", - "@storybook/react": "~6.5.10", - "@storybook/testing-library": "~0.0.13", - "@types/babel__core": "^7", "@types/jest": "^27.4.1", - "eslint": "^8.22.0", + "eslint": "^8.20.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-storybook": "^0.6.4", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "~4.5.5" @@ -31,7 +20,6 @@ "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", - "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", "react": "^17.0.2", "react-dom": "^17.0.2" @@ -41,15 +29,11 @@ "eslint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "jest": "jest", "build": "tsc -p tsconfig.json", - "storybook": "start-storybook -p 6006", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ "/dist" - ], - "dependencies": { - "@rocket.chat/emitter": "next" - } + ] } diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx deleted file mode 100644 index b253f28d949..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.stories.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { MessageDivider, Message, Avatar, Box } from '@rocket.chat/fuselage'; -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import type { ReactElement } from 'react'; - -import '@rocket.chat/icons/dist/rocketchat.css'; -import { VideoConfMessage, VideoConfMessageIcon, VideoConfMessageRow, VideoConfMessageText } from '.'; -import VideoConfMessageAction from './VideoConfMessageAction'; -import VideoConfMessageFooter from './VideoConfMessageFooter'; -import VideoConfMessageFooterText from './VideoConfMessageFooterText'; -import VideoConfMessageSkeleton from './VideoConfMessageSkeleton'; -import VideoConfMessageUserStack from './VideoConfMessageUserStack'; - -export default { - title: 'Components/VideoConfMessage', - component: VideoConfMessage, - decorators: [ - (Story): ReactElement => ( - <Box> - <MessageDivider>May, 24, 2020</MessageDivider> - <Message className='customclass'> - <Message.LeftContainer> - <Avatar url={avatarUrl} size={'x36'} /> - </Message.LeftContainer> - <Message.Container> - <Message.Header> - <Message.Name>Haylie George</Message.Name> - <Message.Username>@haylie.george</Message.Username> - <Message.Role>Admin</Message.Role> - <Message.Role>User</Message.Role> - <Message.Role>Owner</Message.Role> - <Message.Timestamp>12:00 PM</Message.Timestamp> - </Message.Header> - <Message.Body> - <Story /> - </Message.Body> - </Message.Container> - </Message> - </Box> - ), - ], -} as ComponentMeta<typeof VideoConfMessage>; - -const avatarUrl = - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; - -export const CallingDM: ComponentStory<typeof VideoConfMessage> = () => ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon variant='incoming' /> - <VideoConfMessageText>Calling...</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageAction primary>Join</VideoConfMessageAction> - <VideoConfMessageFooterText>Waiting for answer</VideoConfMessageFooterText> - </VideoConfMessageFooter> - </VideoConfMessage> -); - -export const CallEndedDM: ComponentStory<typeof VideoConfMessage> = () => ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon /> - <VideoConfMessageText>Call ended</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageAction>Call Back</VideoConfMessageAction> - <VideoConfMessageFooterText>Call was not answered</VideoConfMessageFooterText> - </VideoConfMessageFooter> - </VideoConfMessage> -); - -export const CallOngoing: ComponentStory<typeof VideoConfMessage> = () => ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon variant='outgoing' /> - <VideoConfMessageText>Call ongoing</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageAction primary>Join</VideoConfMessageAction> - <VideoConfMessageUserStack> - {Array(3) - .fill('') - .map((_, index) => ( - <Avatar key={index} size='x28' url={avatarUrl} /> - ))} - </VideoConfMessageUserStack> - <VideoConfMessageFooterText>Joined</VideoConfMessageFooterText> - </VideoConfMessageFooter> - </VideoConfMessage> -); - -export const CallEnded: ComponentStory<typeof VideoConfMessage> = () => ( - <VideoConfMessage> - <VideoConfMessageRow> - <VideoConfMessageIcon /> - <VideoConfMessageText>Call ended</VideoConfMessageText> - </VideoConfMessageRow> - <VideoConfMessageFooter> - <VideoConfMessageUserStack> - {Array(3) - .fill('') - .map((_, index) => ( - <Avatar key={index} size='x28' url={avatarUrl} /> - ))} - </VideoConfMessageUserStack> - </VideoConfMessageFooter> - </VideoConfMessage> -); - -export const Loading: ComponentStory<typeof VideoConfMessage> = () => <VideoConfMessageSkeleton />; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx deleted file mode 100644 index 0aae5a21ee5..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; - -const VideoConfMessage = ({ ...props }): ReactElement => ( - <Box mbs='x4' maxWidth='345px' borderWidth={2} borderColor='neutral-200' borderRadius='x4' {...props} /> -); - -export default VideoConfMessage; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx deleted file mode 100644 index 49e662833e0..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageAction.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Box, Button } from '@rocket.chat/fuselage'; -import type { AllHTMLAttributes, ReactElement, ReactNode } from 'react'; - -const VideoConfMessageAction = ({ - children, - primary, - ...props -}: { children: ReactNode; primary?: boolean } & Omit<AllHTMLAttributes<HTMLButtonElement>, 'is'>): ReactElement => ( - <Box mi='x4'> - <Button small primary={primary} {...props}> - {children} - </Button> - </Box> -); -export default VideoConfMessageAction; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx deleted file mode 100644 index 1adfc10820b..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooter.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement, ReactNode } from 'react'; - -import VideoConfMessageRow from './VideoConfMessageRow'; - -const VideoConfMessageFooter = ({ children, ...props }: { children: ReactNode }): ReactElement => ( - <VideoConfMessageRow backgroundColor='neutral-100' {...props}> - <Box mi='neg-x4' display='flex' alignItems='center'> - {children} - </Box> - </VideoConfMessageRow> -); - -export default VideoConfMessageFooter; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx deleted file mode 100644 index 975df374ff0..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageFooterText.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement, ReactNode } from 'react'; - -const VideoConfMessageFooterText = ({ children }: { children: ReactNode }): ReactElement => ( - <Box fontScale='c1' mi='x4'> - {children} - </Box> -); -export default VideoConfMessageFooterText; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx deleted file mode 100644 index 7cec0601e77..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageIcon.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; -import type { ReactElement, ComponentProps } from 'react'; - -type VideoConfMessageIconProps = { - variant?: keyof typeof styles; -}; - -const styles = { - ended: { - icon: 'phone-off', - color: 'neutral-700', - backgroundColor: 'neutral-400', - }, - incoming: { - icon: 'phone-in', - color: 'primary-600', - backgroundColor: 'primary-200', - }, - outgoing: { - icon: 'phone', - color: 'success-800', - backgroundColor: 'success-200', - }, -} as const; - -const VideoConfMessageIcon = ({ variant = 'ended' }: VideoConfMessageIconProps): ReactElement => ( - <Box - size='x28' - alignItems='center' - justifyContent='center' - display='flex' - borderRadius='x4' - backgroundColor={styles[variant].backgroundColor} - > - <Icon size='x20' name={styles[variant].icon} color={styles[variant].color} /> - </Box> -); - -export default VideoConfMessageIcon; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx deleted file mode 100644 index b9dd67814f8..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageRow.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; - -const VideoConfMessageRow = ({ ...props }): ReactElement => <Box p='x16' display='flex' alignItems='center' {...props} />; - -export default VideoConfMessageRow; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx deleted file mode 100644 index 18134774eb5..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageSkeleton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Skeleton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; - -import VideoConfMessage from './VideoConfMessage'; -import VideoConfMessageRow from './VideoConfMessageRow'; - -const VideoConfMessageSkeleton = (): ReactElement => ( - <VideoConfMessage> - <VideoConfMessageRow> - <Skeleton width='full' pb='x4' /> - </VideoConfMessageRow> - <VideoConfMessageRow backgroundColor='neutral-100'> - <Skeleton width='full' pb='x4' /> - </VideoConfMessageRow> - </VideoConfMessage> -); - -export default VideoConfMessageSkeleton; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx deleted file mode 100644 index d0cfc3eafce..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageText.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement, ComponentProps } from 'react'; - -const VideoConfMessageText = ({ ...props }: ComponentProps<typeof Box>): ReactElement => <Box fontScale='c2' mis='x8' {...props} />; - -export default VideoConfMessageText; diff --git a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx b/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx deleted file mode 100644 index c555ddb5dc9..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/VideoConfMessageUserStack.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { Children, ReactElement } from 'react'; - -const VideoConfMessageUserStack = ({ children }: { children: ReactElement | ReactElement[] }): ReactElement => ( - <Box mi='x4'> - <Box display='flex' alignItems='center' mi='neg-x2'> - {Children.map(Children.toArray(children), (child, index) => ( - <Box mi='x2' key={index}> - {child} - </Box> - ))} - </Box> - </Box> -); - -export default VideoConfMessageUserStack; diff --git a/packages/ui-video-conf/src/VideoConfMessage/index.ts b/packages/ui-video-conf/src/VideoConfMessage/index.ts deleted file mode 100644 index a76957e92b2..00000000000 --- a/packages/ui-video-conf/src/VideoConfMessage/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import VideoConfMessage from './VideoConfMessage'; -import VideoConfMessageAction from './VideoConfMessageAction'; -import VideoConfMessageFooter from './VideoConfMessageFooter'; -import VideoConfMessageFooterText from './VideoConfMessageFooterText'; -import VideoConfMessageIcon from './VideoConfMessageIcon'; -import VideoConfMessageRow from './VideoConfMessageRow'; -import VideoConfMessageSkeleton from './VideoConfMessageSkeleton'; -import VideoConfMessageText from './VideoConfMessageText'; -import VideoConfMessageUserStack from './VideoConfMessageUserStack'; - -export { - VideoConfMessage, - VideoConfMessageIcon, - VideoConfMessageRow, - VideoConfMessageText, - VideoConfMessageSkeleton, - VideoConfMessageFooter, - VideoConfMessageAction, - VideoConfMessageUserStack, - VideoConfMessageFooterText, -}; diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx index be567f49469..3f93b1a278b 100644 --- a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx +++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx @@ -22,7 +22,7 @@ const VideoConfPopup = forwardRef(function VideoConfPopup( ): ReactElement { return ( <VideoConfPopupContainer ref={ref} position={position}> - <Box p='x24' maxWidth='x276' borderRadius='x4' backgroundColor='white'> + <Box p='x24' maxWidth='x276' backgroundColor='white'> {children} </Box> </VideoConfPopupContainer> diff --git a/packages/ui-video-conf/src/index.ts b/packages/ui-video-conf/src/index.ts index d331731d0f4..e7244b55b21 100644 --- a/packages/ui-video-conf/src/index.ts +++ b/packages/ui-video-conf/src/index.ts @@ -1,7 +1,6 @@ import VideoConfButton from './VideoConfButton'; import VideoConfController from './VideoConfController'; -export * from './VideoConfMessage'; export * from './VideoConfPopup'; export * from './hooks'; export { VideoConfButton, VideoConfController }; diff --git a/yarn.lock b/yarn.lock index 782c95397b1..9275f5dbbce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,7 +84,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.13, @babel/core@npm:^7.19.1, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.1, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": version: 7.19.1 resolution: "@babel/core@npm:7.19.1" dependencies: @@ -1422,7 +1422,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.18.10, @babel/preset-env@npm:^7.19.1": +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.18.9, @babel/preset-env@npm:^7.19.1": version: 7.19.1 resolution: "@babel/preset-env@npm:7.19.1" dependencies: @@ -1977,7 +1977,7 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.3.0, @eslint/eslintrc@npm:^1.3.2": +"@eslint/eslintrc@npm:^1.0.5, @eslint/eslintrc@npm:^1.3.2": version: 1.3.2 resolution: "@eslint/eslintrc@npm:1.3.2" dependencies: @@ -2123,6 +2123,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.10.5": + version: 0.10.5 + resolution: "@humanwhocodes/config-array@npm:0.10.5" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.4 + checksum: af4fa2633c57414be22ddba0a072cc611ef9a07104542fa24bde918a0153b89b6e08ca6a20ccc9079de6079e219e2406e38414d1b662db8bb59a3ba9d6eee6e3 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.5.0": version: 0.5.0 resolution: "@humanwhocodes/config-array@npm:0.5.0" @@ -2134,6 +2145,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.9.2": + version: 0.9.5 + resolution: "@humanwhocodes/config-array@npm:0.9.5" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.4 + checksum: 8ba6281bc0590f6c6eadeefc14244b5a3e3f5903445aadd1a32099ed80e753037674026ce1b3c945ab93561bea5eb29e3c5bff67060e230c295595ba517a3492 + languageName: node + linkType: hard + "@humanwhocodes/gitignore-to-minimatch@npm:^1.0.2": version: 1.0.2 resolution: "@humanwhocodes/gitignore-to-minimatch@npm:1.0.2" @@ -4958,7 +4980,7 @@ __metadata: cron: ~1.8.0 date.js: ~0.3.3 debug: ~4.1.1 - eslint: ^8.22.0 + eslint: ^8.20.0 human-interval: ^2.0.0 jest: ^27.5.1 moment-timezone: ~0.5.27 @@ -4976,7 +4998,7 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/strict-uri-encode": ^2.0.0 - eslint: ^8.22.0 + eslint: ^8.20.0 filter-obj: ^3.0.0 jest: ^27.5.1 query-string: ^7.1.1 @@ -5065,7 +5087,7 @@ __metadata: dependencies: "@types/jest": ^27.4.1 cheerio: 1.0.0-rc.10 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -5081,7 +5103,7 @@ __metadata: "@rocket.chat/icons": next "@rocket.chat/message-parser": next "@rocket.chat/ui-kit": next - eslint: ^8.22.0 + eslint: ^8.20.0 mongodb: ^4.3.1 prettier: ^2.7.1 typescript: ~4.5.5 @@ -5132,7 +5154,7 @@ __metadata: "@types/ws": ^8.5.3 colorette: ^1.4.0 ejson: ^2.2.2 - eslint: ^8.22.0 + eslint: ^8.21.0 eventemitter3: ^4.0.7 fibers: ^5.0.3 jaeger-client: ^3.19.0 @@ -5178,7 +5200,7 @@ __metadata: "@types/prettier": ^2.6.3 "@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/parser": ^5.30.7 - eslint: ^8.22.0 + eslint: ^8.20.0 eslint-config-prettier: ^8.5.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-import: ^2.26.0 @@ -5192,7 +5214,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/favicon@workspace:packages/favicon" dependencies: - eslint: ^8.22.0 + eslint: ^8.20.0 typescript: ~4.5.5 languageName: unknown linkType: soft @@ -5304,7 +5326,25 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-ui-kit@workspace:^, @rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit": +"@rocket.chat/fuselage-ui-kit@npm:next": + version: 0.32.0-dev.104 + resolution: "@rocket.chat/fuselage-ui-kit@npm:0.32.0-dev.104" + dependencies: + "@rocket.chat/ui-kit": ~0.32.0-dev.89 + tslib: ^2.3.1 + peerDependencies: + "@rocket.chat/fuselage": "*" + "@rocket.chat/fuselage-hooks": "*" + "@rocket.chat/fuselage-polyfills": "*" + "@rocket.chat/icons": "*" + "@rocket.chat/styled": "*" + react: ^17.0.2 + react-dom: ^17.0.2 + checksum: a83fb2f2d88bde7128de931ee26a60e2b3b332bf666b6195c72a7222f6b61130f7afd692f63dbfc80562c64295ca6bc47050d7a2dc62a1b7fb1c492b0c554af1 + languageName: node + linkType: hard + +"@rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit": version: 0.0.0-use.local resolution: "@rocket.chat/fuselage-ui-kit@workspace:packages/fuselage-ui-kit" dependencies: @@ -5316,9 +5356,7 @@ __metadata: "@rocket.chat/icons": next "@rocket.chat/prettier-config": next "@rocket.chat/styled": next - "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": next - "@rocket.chat/ui-video-conf": "workspace:^" "@storybook/addon-essentials": ~6.5.10 "@storybook/addons": ~6.5.10 "@storybook/builder-webpack5": ~6.5.10 @@ -5326,36 +5364,29 @@ __metadata: "@storybook/react": ~6.5.10 "@storybook/source-loader": ~6.5.10 "@storybook/theming": ~6.5.10 - "@tanstack/react-query": ^4.2.1 - "@types/react": ~17.0.48 - "@types/react-dom": ~17.0.17 + "@types/react": ~17.0.39 + "@types/react-dom": ^17.0.11 babel-loader: ~8.2.3 cross-env: ^7.0.3 - eslint: ~8.22.0 + eslint: ~8.8.0 lint-staged: ~12.3.3 normalize.css: ^8.0.1 npm-run-all: ^4.1.5 prettier: ~2.5.1 + react: ^17.0.2 react-dom: ^17.0.2 rimraf: ^3.0.2 tslib: ^2.3.1 typescript: ~4.3.5 webpack: ~5.68.0 peerDependencies: - "@rocket.chat/apps-engine": "*" - "@rocket.chat/eslint-config": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/fuselage-polyfills": "*" "@rocket.chat/icons": "*" - "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": "*" - "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": "*" - "@tanstack/react-query": "*" - react: "*" - react-dom: "*" + react: ^17.0.2 + react-dom: ^17.0.2 languageName: unknown linkType: soft @@ -5385,10 +5416,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/gazzodown@workspace:packages/gazzodown" dependencies: - "@babel/core": ~7.18.13 + "@babel/core": ^7.18.9 "@mdx-js/react": ^1.6.22 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next + "@rocket.chat/fuselage": next "@rocket.chat/fuselage-tokens": next "@rocket.chat/message-parser": next "@rocket.chat/styled": next @@ -5408,13 +5440,13 @@ __metadata: "@types/babel__core": ^7 "@types/jest": ^27.4.1 "@types/katex": ~0 - "@types/react": ~17.0.48 - "@types/react-dom": ~17.0.17 + "@types/react": ^17.0.47 + "@types/react-dom": ^18 "@types/testing-library__jest-dom": ^5 "@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/parser": ^5.30.7 babel-loader: ^8.2.5 - eslint: ^8.22.0 + eslint: ^8.20.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -5428,16 +5460,16 @@ __metadata: ts-jest: ^27.1.4 typescript: ~4.5.5 peerDependencies: - "@rocket.chat/core-typings": "*" + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" katex: "*" - react: "*" + react: ~17.0.2 languageName: unknown linkType: soft @@ -5465,7 +5497,7 @@ __metadata: resolution: "@rocket.chat/livechat@workspace:packages/livechat" dependencies: "@babel/eslint-parser": ^7.18.9 - "@babel/preset-env": ^7.18.10 + "@babel/preset-env": ^7.18.9 "@rocket.chat/eslint-config": ^0.4.0 "@rocket.chat/fuselage-tokens": next "@rocket.chat/logo": next @@ -5479,6 +5511,7 @@ __metadata: "@storybook/addon-viewport": ~6.5.10 "@storybook/react": ~6.5.10 "@storybook/theming": ~6.5.10 + "@types/react-dom": ^17.0.17 autoprefixer: ^9.8.8 babel-loader: ^8.1.0 babel-plugin-jsx-pragmatic: ^1.0.2 @@ -5490,7 +5523,7 @@ __metadata: date-fns: ^2.15.0 desvg-loader: ^0.1.0 emoji-mart: ^3.0.1 - eslint: ^8.22.0 + eslint: ^8.20.0 eslint-plugin-import: ^2.26.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -5574,11 +5607,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/meteor@workspace:apps/meteor" dependencies: - "@babel/core": ^7.18.13 + "@babel/core": ^7.18.9 "@babel/eslint-parser": ^7.18.9 "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/preset-env": ^7.18.10 + "@babel/preset-env": ^7.18.9 "@babel/preset-react": ^7.18.6 "@babel/register": ^7.18.9 "@babel/runtime": ^7.18.9 @@ -5607,7 +5640,7 @@ __metadata: "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-toastbar": next "@rocket.chat/fuselage-tokens": next - "@rocket.chat/fuselage-ui-kit": "workspace:^" + "@rocket.chat/fuselage-ui-kit": next "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/icons": next "@rocket.chat/layout": next @@ -5691,7 +5724,7 @@ __metadata: "@types/proxy-from-env": ^1.0.1 "@types/proxyquire": ^1.3.28 "@types/psl": ^1.1.0 - "@types/react": ~17.0.48 + "@types/react": ~17.0.47 "@types/react-dom": ~17.0.17 "@types/rewire": ^2.5.28 "@types/semver": ^7.3.10 @@ -5754,7 +5787,7 @@ __metadata: emailreplyparser: ^0.0.5 emojione: ^4.5.0 emojione-assets: ^4.5.0 - eslint: ^8.22.0 + eslint: ^8.20.0 eslint-config-prettier: ^8.5.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-import: ^2.26.0 @@ -5905,7 +5938,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/node-rsa": ^1.1.1 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 mongodb: ^4.3.1 ts-jest: ^27.1.5 @@ -5919,7 +5952,7 @@ __metadata: dependencies: "@rocket.chat/model-typings": "workspace:^" "@types/jest": ^27.4.1 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 ts-jest: ^27.1.5 typescript: ~4.5.5 @@ -5962,7 +5995,7 @@ __metadata: resolution: "@rocket.chat/poplib@workspace:packages/node-poplib" dependencies: "@types/jest": ^27.4.1 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -6038,7 +6071,7 @@ __metadata: "@rocket.chat/ui-kit": next "@types/jest": ^27.4.1 ajv: ^8.11.0 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 mongodb: ^4.3.1 ts-jest: ^27.1.5 @@ -6123,7 +6156,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" dependencies: - "@babel/core": ~7.18.13 "@rocket.chat/css-in-js": next "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": next @@ -6139,12 +6171,10 @@ __metadata: "@storybook/manager-webpack4": ~6.5.10 "@storybook/react": ~6.5.10 "@storybook/testing-library": ~0.0.13 - "@types/babel__core": ^7 "@types/jest": ^27.4.1 "@types/postcss-url": ^10 - "@types/react": ~17.0.48 - "@types/react-dom": ~17.0.17 - eslint: ^8.22.0 + "@types/react": ~17.0.47 + eslint: ^8.20.0 eslint-plugin-anti-trojan-source: ^1.1.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 @@ -6165,7 +6195,6 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": "*" react: ~17.0.2 languageName: unknown linkType: soft @@ -6211,10 +6240,9 @@ __metadata: "@rocket.chat/fuselage-hooks": next "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 - "@types/react": ~17.0.48 - "@types/react-dom": ~17.0.17 + "@types/react": ^17.0.47 "@types/use-sync-external-store": ^0.0.3 - eslint: ^8.22.0 + eslint: ^8.20.0 jest: ^27.5.1 mongodb: ^4.3.1 react: ~17.0.2 @@ -6238,31 +6266,26 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/ui-kit@npm:~0.32.0-dev.89": + version: 0.32.0-dev.89 + resolution: "@rocket.chat/ui-kit@npm:0.32.0-dev.89" + checksum: 20255ca3b62e288d98eb454375c91c0a494e6e27357f33a61357d6708e8dffdce815c746da03dffef3fc21e04c26ecd53e720a82332f33a532e28a1ac5ac60f4 + languageName: node + linkType: hard + "@rocket.chat/ui-video-conf@workspace:^, @rocket.chat/ui-video-conf@workspace:packages/ui-video-conf": version: 0.0.0-use.local resolution: "@rocket.chat/ui-video-conf@workspace:packages/ui-video-conf" dependencies: - "@babel/core": ~7.18.13 "@rocket.chat/css-in-js": next - "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": next "@rocket.chat/fuselage-hooks": next - "@rocket.chat/icons": next "@rocket.chat/styled": next - "@storybook/addon-actions": ~6.5.10 - "@storybook/addon-docs": ~6.5.10 - "@storybook/addon-essentials": ~6.5.10 - "@storybook/builder-webpack4": ~6.5.10 - "@storybook/manager-webpack4": ~6.5.10 - "@storybook/react": ~6.5.10 - "@storybook/testing-library": ~0.0.13 - "@types/babel__core": ^7 "@types/jest": ^27.4.1 - eslint: ^8.22.0 + eslint: ^8.20.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 - eslint-plugin-storybook: ^0.6.4 jest: ^27.5.1 ts-jest: ^27.1.4 typescript: ~4.5.5 @@ -6270,7 +6293,6 @@ __metadata: "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" - "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 @@ -8130,7 +8152,7 @@ __metadata: languageName: node linkType: hard -"@tanstack/react-query@npm:^4.0.10, @tanstack/react-query@npm:^4.2.1": +"@tanstack/react-query@npm:^4.0.10": version: 4.2.1 resolution: "@tanstack/react-query@npm:4.2.1" dependencies: @@ -9158,7 +9180,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:<18.0.0, @types/react-dom@npm:~17.0.17": +"@types/react-dom@npm:<18.0.0, @types/react-dom@npm:^17.0.11, @types/react-dom@npm:^17.0.17, @types/react-dom@npm:~17.0.17": version: 17.0.17 resolution: "@types/react-dom@npm:17.0.17" dependencies: @@ -9167,7 +9189,16 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^17, @types/react@npm:~17.0.48": +"@types/react-dom@npm:^18": + version: 18.0.6 + resolution: "@types/react-dom@npm:18.0.6" + dependencies: + "@types/react": "*" + checksum: db571047af1a567631758700b9f7d143e566df939cfe5fbf7535347cc0c726a1cdbb5e3f8566d076e54cf708b6c1166689de194a9ba09ee35efc9e1d45911685 + languageName: node + linkType: hard + +"@types/react@npm:*, @types/react@npm:^17": version: 17.0.48 resolution: "@types/react@npm:17.0.48" dependencies: @@ -9178,6 +9209,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^17.0.47, @types/react@npm:~17.0.39, @types/react@npm:~17.0.47": + version: 17.0.50 + resolution: "@types/react@npm:17.0.50" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: b5629dff7c2f3e9fcba95a19b2b3bfd78d7cacc33ba5fc26413dba653d34afcac3b93ddabe563e8062382688a1eac7db68e93739bb8e712d27637a03aaafbbb8 + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -16315,7 +16357,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.1.1": +"eslint-scope@npm:^7.1.0, eslint-scope@npm:^7.1.1": version: 7.1.1 resolution: "eslint-scope@npm:7.1.1" dependencies: @@ -16359,7 +16401,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": +"eslint-visitor-keys@npm:^3.2.0, eslint-visitor-keys@npm:^3.3.0": version: 3.3.0 resolution: "eslint-visitor-keys@npm:3.3.0" checksum: d59e68a7c5a6d0146526b0eec16ce87fbf97fe46b8281e0d41384224375c4e52f5ffb9e16d48f4ea50785cde93f766b0c898e31ab89978d88b0e1720fbfb7808 @@ -16416,12 +16458,12 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.21.0, eslint@npm:^8.22.0": - version: 8.23.1 - resolution: "eslint@npm:8.23.1" +"eslint@npm:^8.20.0": + version: 8.24.0 + resolution: "eslint@npm:8.24.0" dependencies: "@eslint/eslintrc": ^1.3.2 - "@humanwhocodes/config-array": ^0.10.4 + "@humanwhocodes/config-array": ^0.10.5 "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 "@humanwhocodes/module-importer": ^1.0.1 ajv: ^6.10.0 @@ -16461,17 +16503,18 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: a727e15492786a03b438bcf021db49f715680679846a7b8d79b98ad34576f2a570404ffe882d3c3e26f6359bff7277ef11fae5614bfe8629adb653f20d018c71 + checksum: ca293ce7116599b742d7ab4d43db469beec22f40dd272092d809498be3cff3a7c567769f9763bdf6799aac13dd53447b93a99629b7b54092783046eb57eaced6 languageName: node linkType: hard -"eslint@npm:~8.22.0": - version: 8.22.0 - resolution: "eslint@npm:8.22.0" +"eslint@npm:^8.21.0, eslint@npm:^8.22.0": + version: 8.23.1 + resolution: "eslint@npm:8.23.1" dependencies: - "@eslint/eslintrc": ^1.3.0 + "@eslint/eslintrc": ^1.3.2 "@humanwhocodes/config-array": ^0.10.4 "@humanwhocodes/gitignore-to-minimatch": ^1.0.2 + "@humanwhocodes/module-importer": ^1.0.1 ajv: ^6.10.0 chalk: ^4.0.0 cross-spawn: ^7.0.2 @@ -16481,13 +16524,12 @@ __metadata: eslint-scope: ^7.1.1 eslint-utils: ^3.0.0 eslint-visitor-keys: ^3.3.0 - espree: ^9.3.3 + espree: ^9.4.0 esquery: ^1.4.0 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 file-entry-cache: ^6.0.1 find-up: ^5.0.0 - functional-red-black-tree: ^1.0.1 glob-parent: ^6.0.1 globals: ^13.15.0 globby: ^11.1.0 @@ -16496,6 +16538,7 @@ __metadata: import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 + js-sdsl: ^4.1.4 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 @@ -16507,10 +16550,54 @@ __metadata: strip-ansi: ^6.0.1 strip-json-comments: ^3.1.0 text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: a727e15492786a03b438bcf021db49f715680679846a7b8d79b98ad34576f2a570404ffe882d3c3e26f6359bff7277ef11fae5614bfe8629adb653f20d018c71 + languageName: node + linkType: hard + +"eslint@npm:~8.8.0": + version: 8.8.0 + resolution: "eslint@npm:8.8.0" + dependencies: + "@eslint/eslintrc": ^1.0.5 + "@humanwhocodes/config-array": ^0.9.2 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.1.0 + eslint-utils: ^3.0.0 + eslint-visitor-keys: ^3.2.0 + espree: ^9.3.0 + esquery: ^1.4.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + functional-red-black-tree: ^1.0.1 + glob-parent: ^6.0.1 + globals: ^13.6.0 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.0.4 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + regexpp: ^3.2.0 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 v8-compile-cache: ^2.0.3 bin: eslint: bin/eslint.js - checksum: 2d84a7a2207138cdb250759b047fdb05a57fede7f87b7a039d9370edba7f26e23a873a208becfd4b2c9e4b5499029f3fc3b9318da3290e693d25c39084119c80 + checksum: 41a7e85bf84cf9f2d758ef3e8d08020a39a2836703728b59535684681349bd021c2c6e24174462b844a914870d707d2151e0371198899d957b444de91adaa435 languageName: node linkType: hard @@ -16525,7 +16612,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.3.3, espree@npm:^9.4.0": +"espree@npm:^9.3.0, espree@npm:^9.4.0": version: 9.4.0 resolution: "espree@npm:9.4.0" dependencies: @@ -29353,7 +29440,7 @@ __metadata: languageName: node linkType: hard -"react@npm:~17.0.2": +"react@npm:^17.0.2, react@npm:~17.0.2": version: 17.0.2 resolution: "react@npm:17.0.2" dependencies: -- GitLab From 4ecf45b16a288a98134cc320de42b007682b8e32 Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Thu, 29 Sep 2022 13:45:25 -0600 Subject: [PATCH 084/107] Regression: Typo on livechat/queue endpoint client call (#26962) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../client/views/omnichannel/queueList/hooks/useQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts index 76a5fe26dfc..9d296e31991 100644 --- a/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts +++ b/apps/meteor/client/views/omnichannel/queueList/hooks/useQuery.ts @@ -25,7 +25,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe useMemo(() => { const query: { agentId?: string; - includeOflineAgents?: 'true' | 'false'; + includeOfflineAgents?: 'true' | 'false'; departmentId?: string; sort: string; count: number; @@ -39,7 +39,7 @@ export const useQuery: useQueryType = ({ servedBy, status, departmentId, itemsPe }; if (status !== 'online') { - query.includeOflineAgents = 'true'; + query.includeOfflineAgents = 'true'; } if (servedBy) { query.agentId = servedBy; -- GitLab From 4f27e21b75ce6baf56f23244ec2a48151291e35b Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Thu, 29 Sep 2022 15:33:20 -0600 Subject: [PATCH 085/107] Regression: Remove symbols from number before storing PBX event (#26969) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- apps/meteor/server/services/omnichannel-voip/service.ts | 9 ++++++++- .../voip/connector/asterisk/ami/ContinuousMonitor.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 66e6705a6f8..19de573d24a 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -115,9 +115,16 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn */ // Use latest queue caller join event + const numericPhone = guest?.phone?.[0]?.phoneNumber.replace(/\D/g, ''); const callStartPbxEvent = await PbxEvents.findOne( { - phone: guest?.phone?.[0]?.phoneNumber, + $or: [ + { + phone: numericPhone, // Incoming calls will have phone number (connectedlinenum) without any symbol + }, + { phone: `*${numericPhone}` }, // Outgoing calls will have phone number (connectedlinenum) with * prefix + { phone: `+${numericPhone}` }, // Just in case + ], event: { $in: ['QueueCallerJoin', 'DialEnd', 'DialState'], }, diff --git a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts index 91345e0935d..2aef44cd957 100644 --- a/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts +++ b/apps/meteor/server/services/voip/connector/asterisk/ami/ContinuousMonitor.ts @@ -268,7 +268,7 @@ export class ContinuousMonitor extends Command { return; } - if (event.dialstatus.toLowerCase() !== 'answer' && event.dialstatus.toLowerCase() !== 'ringing') { + if (!['answer', 'ringing'].includes(event.dialstatus.toLowerCase())) { this.logger.warn(`Received unexpected event ${event.event} dialstatus = ${event.dialstatus}`); return; } @@ -283,7 +283,7 @@ export class ContinuousMonitor extends Command { uniqueId: `${event.event}-${event.calleridnum}-${event.channel}-${event.destchannel}-${event.uniqueid}`, event: event.event, ts: new Date(), - phone: event?.connectedlinenum, + phone: event?.connectedlinenum.replace(/\D/g, ''), // Remove all non-numeric characters callUniqueId: event.uniqueid, callUniqueIdFallback: event.linkedid, agentExtension: event.calleridnum, -- GitLab From d70fda2d10a5661dbddb137fc0482d332af57285 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Thu, 29 Sep 2022 19:40:37 -0300 Subject: [PATCH 086/107] Regression: Event handler blocking mention links (#26964) * Bypass event handler blocking mention links * remove typo * Use default param value on `getCommonRoomEvents` Co-authored-by: yash-rajpal <rajpal.yash03@gmail.com> --- .../views/app/lib/getCommonRoomEvents.ts | 4 +-- .../components/message/Metrics/Discussion.tsx | 4 +-- .../components/MessageContentBody.tsx | 28 +++++++++++++++++-- .../views/room/components/body/RoomBody.tsx | 6 ++-- .../views/room/contexts/MessageContext.ts | 2 +- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts index 7c49c0cf877..00ee5312c7c 100644 --- a/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts +++ b/apps/meteor/app/ui/client/views/app/lib/getCommonRoomEvents.ts @@ -333,7 +333,7 @@ function handleMentionLinkClick(event: JQuery.ClickEvent, template: CommonRoomTe } } -export const getCommonRoomEvents = () => ({ +export const getCommonRoomEvents = (useLegacyMessageTemplate = true) => ({ ...createMessageTouchEvents(), 'click [data-message-action]': handleMessageActionButtonClick, 'click .js-follow-thread': handleFollowThreadButtonClick, @@ -345,5 +345,5 @@ export const getCommonRoomEvents = () => ({ 'click .js-actionButton-respondWithQuotedMessage': handleRespondWithQuotedMessageActionButtonClick, 'click .js-actionButton-sendMessage': handleSendMessageActionButtonClick, 'click .message-actions__menu': handleMessageActionMenuClick, - 'click .mention-link': handleMentionLinkClick, + ...(useLegacyMessageTemplate && { 'click .mention-link': handleMentionLinkClick }), }); diff --git a/apps/meteor/client/components/message/Metrics/Discussion.tsx b/apps/meteor/client/components/message/Metrics/Discussion.tsx index d375030f371..41b6cbb8e16 100644 --- a/apps/meteor/client/components/message/Metrics/Discussion.tsx +++ b/apps/meteor/client/components/message/Metrics/Discussion.tsx @@ -1,6 +1,6 @@ import { Message } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; +import React, { FC, UIEvent } from 'react'; import { useTimeAgo } from '../../../hooks/useTimeAgo'; import { useBlockRendered } from '../hooks/useBlockRendered'; @@ -8,7 +8,7 @@ import { useBlockRendered } from '../hooks/useBlockRendered'; type DicussionOptions = { drid: string; rid: string; - openDiscussion: () => void; + openDiscussion: (event: UIEvent) => void; count: number; lm?: Date; }; diff --git a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx index 6a30b8f8fa3..2e666792f35 100644 --- a/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx +++ b/apps/meteor/client/views/room/MessageList/components/MessageContentBody.tsx @@ -3,9 +3,12 @@ import { Box, MessageBody } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { MarkupInteractionContext, Markup, UserMention, ChannelMention } from '@rocket.chat/gazzodown'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import React, { ReactElement, useCallback, useMemo } from 'react'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import React, { ReactElement, UIEvent, useCallback, useMemo } from 'react'; import { emoji } from '../../../../../app/emoji/client'; +import { fireGlobalEvent } from '../../../../lib/utils/fireGlobalEvent'; import { useMessageActions } from '../../contexts/MessageContext'; import { useMessageListHighlights } from '../contexts/MessageListContext'; import { MessageWithMdEnforced } from '../lib/parseMessageTextToAstMarkdown'; @@ -61,14 +64,33 @@ const MessageContentBody = ({ mentions, channels, md }: MessageContentBodyProps) return; } - return openUserCard(username); + return (event: UIEvent): void => { + event.stopPropagation(); + openUserCard(username)(event); + }; }, [openUserCard], ); const resolveChannelMention = useCallback((mention: string) => channels?.find(({ name }) => name === mention), [channels]); - const onChannelMentionClick = useCallback(({ _id: rid }: ChannelMention) => openRoom(rid), [openRoom]); + const { isEmbedded } = useLayout(); + + const onChannelMentionClick = useCallback( + ({ _id: rid }: ChannelMention) => + (event: UIEvent): void => { + if (isEmbedded) { + fireGlobalEvent('click-mention-link', { + path: FlowRouter.path('channel', { name: rid }), + channel: rid, + }); + } + + event.stopPropagation(); + openRoom(rid)(event); + }, + [isEmbedded, openRoom], + ); // TODO: this style should go to Fuselage <MessageBody> repository const messageBodyAdditionalStyles = css` diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index d8c4f7c1d2a..7ce0d4fa656 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -63,7 +63,7 @@ const RoomBody = (): ReactElement => { const hideFlexTab = useUserPreference<boolean>('hideFlexTab'); const hideUsernames = useUserPreference<boolean>('hideUsernames'); const displayAvatars = useUserPreference<boolean>('displayAvatars'); - const useLegacyMessageTemplate = useUserPreference<boolean>('useLegacyMessageTemplate'); + const useLegacyMessageTemplate = useUserPreference<boolean>('useLegacyMessageTemplate') ?? false; const viewMode = useUserPreference<number>('messageViewMode'); const wrapperRef = useRef<HTMLDivElement | null>(null); @@ -329,7 +329,7 @@ const RoomBody = (): ReactElement => { } const messageEvents: Record<string, (event: any, template: CommonRoomTemplateInstance) => void> = { - ...getCommonRoomEvents(), + ...getCommonRoomEvents(useLegacyMessageTemplate), 'click .toggle-hidden'(event: JQuery.ClickEvent) { const mid = event.target.dataset.message; if (mid) document.getElementById(mid)?.classList.toggle('message--ignored'); @@ -361,7 +361,7 @@ const RoomBody = (): ReactElement => { $(messageList).off(event, selector, listener); } }; - }, [room._id, sendToBottomIfNecessary, toolbox]); + }, [room._id, sendToBottomIfNecessary, toolbox, useLegacyMessageTemplate]); useEffect(() => { const wrapper = wrapperRef.current; diff --git a/apps/meteor/client/views/room/contexts/MessageContext.ts b/apps/meteor/client/views/room/contexts/MessageContext.ts index b5edcced81c..3a31fae2094 100644 --- a/apps/meteor/client/views/room/contexts/MessageContext.ts +++ b/apps/meteor/client/views/room/contexts/MessageContext.ts @@ -22,7 +22,7 @@ export type MessageContextValue = { oembedEnabled: boolean; actions: { openUserCard: (username: string) => (e: UIEvent) => void; - openRoom: (id: string) => () => void; + openRoom: (id: string) => (event: UIEvent) => void; openThread: (tmid: string, jump?: string) => (e: MouseEvent) => void; runActionLink: (message: IMessage) => (action: string) => () => void; replyBroadcast: (message: IMessage) => void; -- GitLab From 96290c9588c9aac01d1235113cbd9d2981ff40dc Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Fri, 30 Sep 2022 06:55:41 +0530 Subject: [PATCH 087/107] Regression: Incorrect on-hold chat resume message (#26935) Co-authored-by: Kevin Aleman <kaleman960@gmail.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../ComposerOmnichannel/ComposerOmnichannelOnHold.tsx | 4 +++- packages/ui-contexts/src/ServerContext/methods.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx index a0fde3c5874..f8c74997903 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx @@ -12,7 +12,9 @@ export const ComposerOmnichannelOnHold = (): ReactElement => { <footer className='rc-message-box footer'> <MessageFooterCallout> <MessageFooterCalloutContent>{t('chat_on_hold_due_to_inactivity')}</MessageFooterCalloutContent> - <MessageFooterCalloutAction onClick={(): Promise<unknown> => resume(room._id)}>{t('Resume')}</MessageFooterCalloutAction> + <MessageFooterCalloutAction onClick={(): Promise<unknown> => resume(room._id, { clientAction: true })}> + {t('Resume')} + </MessageFooterCalloutAction> </MessageFooterCallout> </footer> ); diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 8b297bd2a40..172008d9abd 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -227,7 +227,7 @@ export interface ServerMethods { 'livechat:changeLivechatStatus': (params?: void | { status?: string; agentId?: string }) => unknown; 'livechat:saveAgentInfo': (_id: string, agentData: unknown, agentDepartments: unknown) => unknown; 'livechat:takeInquiry': (inquiryId: string) => unknown; - 'livechat:resumeOnHold': (roomId: string) => unknown; + 'livechat:resumeOnHold': (roomId: string, options?: { clientAction: boolean }) => unknown; 'autoTranslate.getProviderUiMetadata': () => Record<string, { name: string; displayName: string }>; 'autoTranslate.getSupportedLanguages': (language: string) => ISupportedLanguage[]; 'spotlight': ( -- GitLab From 0e6bc45d74d94f93810fce8697dfc4aec36f741d Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 30 Sep 2022 00:52:07 -0300 Subject: [PATCH 088/107] [IMPROVE] Results of user auto complete (#26687) --- apps/meteor/app/api/server/lib/users.ts | 30 ++-- apps/meteor/app/api/server/v1/users.ts | 16 +- apps/meteor/server/lib/spotlight.js | 2 +- .../meteor/server/models/raw/Subscriptions.ts | 2 + apps/meteor/tests/end-to-end/api/01-users.js | 149 +++++++++++++++--- .../src/models/ISubscriptionsModel.ts | 3 +- 6 files changed, 170 insertions(+), 32 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 38edb4fa5cd..8fed9e4e215 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -1,28 +1,24 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { IUser } from '@rocket.chat/core-typings'; import type { Filter } from 'mongodb'; -import { Users } from '@rocket.chat/models'; +import { Users, Subscriptions } from '@rocket.chat/models'; import type { Mongo } from 'meteor/mongo'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../settings/server'; type UserAutoComplete = Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>; + export async function findUsersToAutocomplete({ uid, selector, }: { uid: string; - selector: { - exceptions: string[]; - conditions: Filter<IUser>; - term: string; - }; + selector: { exceptions: Required<IUser>['username'][]; conditions: Filter<IUser>; term: string }; }): Promise<{ items: UserAutoComplete[]; }> { - if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { - return { items: [] }; - } + const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(','); const exceptions = selector.exceptions || []; const conditions = selector.conditions || {}; const options = { @@ -39,6 +35,20 @@ export async function findUsersToAutocomplete({ limit: 10, }; + // Search on DMs first, to list known users before others. + const contacts = await Subscriptions.findConnectedUsersExcept(uid, selector.term, exceptions, searchFields, conditions, 10, 'd'); + if (contacts.length >= options.limit) { + return { items: contacts as UserAutoComplete[] }; + } + + options.limit -= contacts.length; + contacts.forEach(({ username }) => exceptions.push(username)); + + if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { + const users = await Subscriptions.findConnectedUsersExcept(uid, selector.term, exceptions, searchFields, conditions, 10); + return { items: contacts.concat(users) as UserAutoComplete[] }; + } + const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions<UserAutoComplete>( new RegExp(escapeRegExp(selector.term), 'i'), exceptions, @@ -47,7 +57,7 @@ export async function findUsersToAutocomplete({ ).toArray(); return { - items: users, + items: (contacts as UserAutoComplete[]).concat(users), }; } diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 858881ae7f7..3ee5698eb84 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -18,6 +18,7 @@ import { Match, check } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { IExportOperation, IPersonalAccessToken, IUser } from '@rocket.chat/core-typings'; import { Users as UsersRaw } from '@rocket.chat/models'; +import type { Filter } from 'mongodb'; import { Users, Subscriptions } from '../../../models/server'; import { hasPermission } from '../../../authorization/server'; @@ -815,11 +816,22 @@ API.v1.addRoute( { authRequired: true, validateParams: isUsersAutocompleteProps }, { async get() { - const { selector } = this.queryParams; + const { selector: selectorRaw } = this.queryParams; + + const selector: { exceptions: Required<IUser>['username'][]; conditions: Filter<IUser>; term: string } = JSON.parse(selectorRaw); + + try { + if (selector?.conditions && !isValidQuery(selector.conditions, ['*'], ['$or', '$and'])) { + throw new Error('error-invalid-query'); + } + } catch (e) { + return API.v1.failure(e); + } + return API.v1.success( await findUsersToAutocomplete({ uid: this.userId, - selector: JSON.parse(selector), + selector, }), ); }, diff --git a/apps/meteor/server/lib/spotlight.js b/apps/meteor/server/lib/spotlight.js index 125feab8b42..ea645cba673 100644 --- a/apps/meteor/server/lib/spotlight.js +++ b/apps/meteor/server/lib/spotlight.js @@ -102,7 +102,7 @@ export class Spotlight { users.push( ...Promise.await( - SubscriptionsRaw.findConnectedUsersExcept(userId, text, usernames, searchFields, options.limit || 5, roomType, match), + SubscriptionsRaw.findConnectedUsersExcept(userId, text, usernames, searchFields, {}, options.limit || 5, roomType, match), { readPreference: options.readPreference, }, diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 82ac9dd6adc..8eaf599754e 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -256,6 +256,7 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri searchTerm: string, exceptions: string[], searchFields: string[], + extraConditions: Filter<IUser>, limit: number, roomType?: ISubscription['t'], { startsWith = false, endsWith = false }: { startsWith?: string | false; endsWith?: string | false } = {}, @@ -319,6 +320,7 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri { $match: { $expr: { $eq: ['$_id', '$$id'] }, + ...extraConditions, active: true, username: { $exists: true, diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index ec49e2bf7bc..0a3360214db 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -28,7 +28,7 @@ function createTestUser() { request .post(api('users.create')) .set(credentials) - .send({ email, name: username, username, password }) + .send({ email, name: username, username, password, joinDefaultChannels: false }) .end((err, res) => resolve(res.body.user)); }); } @@ -65,6 +65,20 @@ function deleteTestUser(user) { }); } +async function createChannel(userCredentials, name) { + const res = await request.post(api('channels.create')).set(userCredentials).send({ + name, + }); + + return res.body.channel._id; +} + +async function joinChannel(userCredentials, roomId) { + return request.post(api('channels.join')).set(userCredentials).send({ + roomId, + }); +} + describe('[Users]', function () { this.retries(0); @@ -3092,11 +3106,35 @@ describe('[Users]', function () { }); describe('[/users.autocomplete]', () => { - it('should return an empty list when the user does not have the necessary permission', (done) => { - updatePermission('view-outside-room', []).then(() => { + after(() => { + updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']); + }); + + describe('[without permission]', () => { + let user; + let userCredentials; + let user2; + let user2Credentials; + let roomId; + + this.timeout(20000); + + before(async () => { + user = await createTestUser(); + user2 = await createTestUser(); + + userCredentials = await loginTestUser(user); + user2Credentials = await loginTestUser(user2); + + await updatePermission('view-outside-room', []); + + roomId = await createChannel(userCredentials, `channel.autocomplete.${Date.now()}`); + }); + + it('should return an empty list when the user does not have any subscription', (done) => { request .get(api('users.autocomplete?selector={}')) - .set(credentials) + .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { @@ -3105,33 +3143,108 @@ describe('[Users]', function () { }) .end(done); }); + + it('should return users that are subscribed to the same rooms as the requester', async () => { + await joinChannel(user2Credentials, roomId); + + request + .get(api('users.autocomplete?selector={}')) + .set(userCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items').and.to.be.an('array').with.lengthOf(1); + }); + }); }); - it('should return an error when the required parameter "selector" is not provided', (done) => { - updatePermission('view-outside-room', ['admin', 'user']).then(() => { + + describe('[with permission]', () => { + before(() => { + updatePermission('view-outside-room', ['admin', 'user']); + }); + + it('should return an error when the required parameter "selector" is not provided', () => { request .get(api('users.autocomplete')) .set(credentials) .query({}) .expect('Content-Type', 'application/json') .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }); + }); + it('should return the users to fill auto complete', (done) => { + request + .get(api('users.autocomplete?selector={}')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items').and.to.be.an('array'); + }) + .end(done); + }); + + it('should filter results when using allowed operators', (done) => { + request + .get(api('users.autocomplete')) + .set(credentials) + .query({ + selector: JSON.stringify({ + conditions: { + $and: [ + { + active: false, + }, + { + status: 'online', + }, + ], + }, + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items').and.to.be.an('array').with.lengthOf(0); + }) + .end(done); + }); + + it('should return an error when using forbidden operators', (done) => { + request + .get(api('users.autocomplete')) + .set(credentials) + .query({ + selector: JSON.stringify({ + conditions: { + $nor: [ + { + username: { + $exists: false, + }, + }, + { + status: { + $exists: false, + }, + }, + ], + }, + }), + }) + .expect('Content-Type', 'application/json') + .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }) .end(done); }); }); - it('should return the users to fill auto complete', (done) => { - request - .get(api('users.autocomplete?selector={}')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('items').and.to.be.an('array'); - }) - .end(done); - }); }); describe('[/users.getStatus]', () => { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 63702bc8b10..0809f9361a7 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -1,4 +1,4 @@ -import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions } from 'mongodb'; +import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter } from 'mongodb'; import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; @@ -64,6 +64,7 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> { searchTerm: string, exceptions: string[], searchFields: string[], + extraConditions: Filter<IUser>, limit: number, roomType?: ISubscription['t'], { startsWith, endsWith }?: { startsWith?: string | false; endsWith?: string | false }, -- GitLab From 26941d5defedf0e04bb4053e5a27ee6ec5995a89 Mon Sep 17 00:00:00 2001 From: Martin Schoeler <martin.schoeler@rocket.chat> Date: Mon, 3 Oct 2022 17:23:20 -0300 Subject: [PATCH 089/107] Regression: Composer not reactive when omnichannel room closed (#26983) * Regression: Composer not reactive when omnichannel room closed * Add TODO Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../composer/ComposerOmnichannel/ComposerOmnichannel.tsx | 4 +++- apps/meteor/client/views/room/contexts/RoomContext.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx index 064c72b8ac2..3c4043428e2 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx @@ -14,6 +14,7 @@ export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement = const isSubscribed = useUserIsSubscribed(); const [isInquired, setIsInquired] = useState(() => !servedBy && queuedAt); + const [isOpen, setIsOpen] = useState(() => open); const subscribeToRoom = useStream('room-data'); @@ -22,6 +23,7 @@ export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement = useEffect(() => { subscribeToRoom(_id, (entry: IOmnichannelRoom) => { setIsInquired(!entry.servedBy && entry.queuedAt); + setIsOpen(entry.open); }); }, [_id, subscribeToRoom]); @@ -29,7 +31,7 @@ export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement = setIsInquired(!servedBy && queuedAt); }, [queuedAt, servedBy, _id]); - if (!open) { + if (!isOpen) { return ( <footer className='rc-message-box footer'> <MessageFooterCallout>{t('This_conversation_is_already_closed')}</MessageFooterCallout> diff --git a/apps/meteor/client/views/room/contexts/RoomContext.ts b/apps/meteor/client/views/room/contexts/RoomContext.ts index 368b20d3279..f6c55619c0f 100644 --- a/apps/meteor/client/views/room/contexts/RoomContext.ts +++ b/apps/meteor/client/views/room/contexts/RoomContext.ts @@ -61,6 +61,11 @@ export const useRoomMessages = (): { }; export const useOmnichannelRoom = (): IOmnichannelRoom => { + // TODO: today if the user do not belong in the room, the room object will not update on new changes + // for normal rooms this is OK, but for Omnichannel rooms, + // there are cases where an agent can be outside of the room but need to see the room changes + // A solution would be to use subscribeToRoom to get the room updates + const { room } = useContext(RoomContext) || {}; if (!room) { -- GitLab From aa249a63ab85b68231f0de9bbf545b33fc3812fe Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Tue, 4 Oct 2022 01:53:12 -0300 Subject: [PATCH 090/107] Regression: Handle `undefined` values on `useReactiveQuery`'s query function (#26988) --- .../app/ui-utils/client/lib/openRoom.tsx | 1 - apps/meteor/client/hooks/useReactiveQuery.ts | 70 ++++++------------ apps/meteor/client/startup/index.ts | 2 +- ...Cache.ts => mergeSubscriptionsAndRooms.ts} | 9 --- .../hooks/usePutChatOnHoldMutation.ts | 26 +++++++ .../QuickActions/hooks/useQuickActions.tsx | 74 +++++++++++-------- .../hooks/useReturnChatToQueueMutation.ts | 26 +++++++ .../views/room/components/body/RoomBody.tsx | 2 +- .../body/composer/ComposerContainer.tsx | 2 +- .../ComposerOmnichannel.tsx | 18 +++-- .../ComposerOmnichannelOnHold.tsx | 13 +++- .../hooks/useResumeChatOnHoldMutation.ts | 26 +++++++ .../views/room/providers/RoomProvider.tsx | 19 ++++- apps/meteor/definition/methods/omnichannel.ts | 4 +- 14 files changed, 186 insertions(+), 106 deletions(-) rename apps/meteor/client/startup/{queryCache.ts => mergeSubscriptionsAndRooms.ts} (96%) create mode 100644 apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts create mode 100644 apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts create mode 100644 apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts diff --git a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx index 5d54f54c890..04dc47f08aa 100644 --- a/apps/meteor/app/ui-utils/client/lib/openRoom.tsx +++ b/apps/meteor/app/ui-utils/client/lib/openRoom.tsx @@ -107,7 +107,6 @@ export async function openRoom(type: RoomType, name: string, render = true) { console.error(error); } } - Session.set('roomNotFound', { type, name, error }); appLayout.render( <MainLayout> <RoomNotFound /> diff --git a/apps/meteor/client/hooks/useReactiveQuery.ts b/apps/meteor/client/hooks/useReactiveQuery.ts index 02f6061843c..b7c6d2061d5 100644 --- a/apps/meteor/client/hooks/useReactiveQuery.ts +++ b/apps/meteor/client/hooks/useReactiveQuery.ts @@ -1,8 +1,9 @@ import { IRole, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; -import { useQuery, UseQueryOptions, QueryKey, UseQueryResult, useQueryClient, QueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useQuery, UseQueryOptions, QueryKey, UseQueryResult, useQueryClient } from '@tanstack/react-query'; +import { Tracker } from 'meteor/tracker'; import { Roles, RoomRoles, Rooms, Subscriptions, Users } from '../../app/models/client'; +import { queueMicrotask } from '../lib/utils/queueMicrotask'; // For convenience as we want to minimize references to the old client models const queryableCollections = { @@ -13,58 +14,35 @@ const queryableCollections = { roomRoles: RoomRoles as Mongo.Collection<Pick<ISubscription, 'rid' | 'u' | 'roles'>>, } as const; -const dep = new Tracker.Dependency(); -const reactiveSources = new Set<{ - reactiveQueryFn: (collections: typeof queryableCollections) => unknown | undefined; - queryClient: QueryClient; - queryKey: QueryKey; -}>(); - -export const runReactiveFunctions = (): void => { - if (!Tracker.currentComputation) { - throw new Error('runReactiveFunctions must be called inside a Tracker.autorun'); - } - - dep.depend(); - - for (const { reactiveQueryFn, queryClient, queryKey } of reactiveSources) { - // This tracker will be invalidated when the query data changes - Tracker.autorun((c) => { - const data = reactiveQueryFn(queryableCollections); - if (!c.firstRun) queryClient.setQueryData(queryKey, data); - }); - } -}; - -// While React Query handles all async stuff, we need to handle the reactive stuff ourselves using effects export const useReactiveQuery = <TQueryFnData, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>( queryKey: TQueryKey, - reactiveQueryFn: (collections: typeof queryableCollections) => TQueryFnData | undefined, + reactiveQueryFn: (collections: typeof queryableCollections) => TQueryFnData, options?: UseQueryOptions<TQueryFnData, Error, TData, TQueryKey>, ): UseQueryResult<TData, Error> => { const queryClient = useQueryClient(); - useEffect(() => { - const reactiveSource = { reactiveQueryFn, queryClient, queryKey }; - - reactiveSources.add(reactiveSource); - dep.changed(); - - return (): void => { - reactiveSources.delete(reactiveSource); - dep.changed(); - }; - }); - return useQuery( queryKey, - (): Promise<TQueryFnData> => { - const result = Tracker.nonreactive(() => reactiveQueryFn(queryableCollections)); - - if (result) return Promise.resolve(result); - - return new Promise(() => undefined); - }, + (): Promise<TQueryFnData> => + new Promise((resolve, reject) => { + queueMicrotask(() => { + Tracker.autorun((c) => { + const data = reactiveQueryFn(queryableCollections); + + if (c.firstRun) { + if (data === undefined) { + reject(new Error('Reactive query returned undefined')); + } else { + resolve(data); + } + return; + } + + queryClient.refetchQueries(queryKey, { exact: true }); + c.stop(); + }); + }); + }), { staleTime: Infinity, ...options }, ); }; diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index d980ec9dc47..86212df4ece 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -14,13 +14,13 @@ import './incomingMessages'; import './ldap'; import './loadMissedMessages'; import './loginViaQuery'; +import './mergeSubscriptionsAndRooms'; import './messageObserve'; import './messageTypes'; import './notifications'; import './oauth'; import './openedRoom'; import './otr'; -import './queryCache'; import './readMessage'; import './readReceipt'; import './reloadRoomAfterLogin'; diff --git a/apps/meteor/client/startup/queryCache.ts b/apps/meteor/client/startup/mergeSubscriptionsAndRooms.ts similarity index 96% rename from apps/meteor/client/startup/queryCache.ts rename to apps/meteor/client/startup/mergeSubscriptionsAndRooms.ts index b2fe0191be9..4d1283e3758 100644 --- a/apps/meteor/client/startup/queryCache.ts +++ b/apps/meteor/client/startup/mergeSubscriptionsAndRooms.ts @@ -1,12 +1,9 @@ import { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy, ISubscription } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; -import { Tracker } from 'meteor/tracker'; import { Rooms, Subscriptions } from '../../app/models/client'; import { callbacks } from '../../lib/callbacks'; import { SubscriptionWithRoom } from '../definitions/SubscriptionWithRoom'; -import { runReactiveFunctions } from '../hooks/useReactiveQuery'; const getLowerCaseNames = ( room: Pick<IRoom, 'name' | 'fname' | 'prid'>, @@ -180,9 +177,3 @@ callbacks.add('cachedCollection-loadFromServer-rooms', mergeRoomSub); callbacks.add('cachedCollection-received-subscriptions', mergeSubRoom); callbacks.add('cachedCollection-sync-subscriptions', mergeSubRoom); callbacks.add('cachedCollection-loadFromServer-subscriptions', mergeSubRoom); - -Meteor.startup(() => { - Tracker.autorun(() => { - runReactiveFunctions(); - }); -}); diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts new file mode 100644 index 00000000000..d31fb4d9aa6 --- /dev/null +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts @@ -0,0 +1,26 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation, UseMutationOptions, UseMutationResult, useQueryClient } from '@tanstack/react-query'; + +export const usePutChatOnHoldMutation = ( + options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>, +): UseMutationResult<void, Error, IRoom['_id']> => { + const putChatOnHold = useEndpoint('POST', '/v1/livechat/room.onHold'); + + const queryClient = useQueryClient(); + + return useMutation( + async (rid) => { + await putChatOnHold({ roomId: rid }); + }, + { + ...options, + onSuccess: async (data, rid, context) => { + await queryClient.invalidateQueries(['current-chats']); + await queryClient.invalidateQueries(['rooms', rid]); + await queryClient.invalidateQueries(['subscriptions', { rid }]); + return options?.onSuccess?.(data, rid, context); + }, + }, + ); +}; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 5d18c9ee74d..a095249edd2 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -24,6 +24,8 @@ import TranscriptModal from '../../../../../../components/Omnichannel/modals/Tra import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; import { QuickActionsActionConfig, QuickActionsEnum } from '../../../../lib/QuickActions'; import { useQuickActionsContext } from '../../../../lib/QuickActions/QuickActionsContext'; +import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; +import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; export const useQuickActions = ( room: IOmnichannelRoom, @@ -33,7 +35,7 @@ export const useQuickActions = ( getAction: (id: string) => void; } => { const setModal = useSetModal(); - const route = useRoute('home'); + const homeRoute = useRoute('home'); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -71,24 +73,6 @@ export const useQuickActions = ( const closeModal = useCallback(() => setModal(null), [setModal]); - const closeOnHoldModal = useCallback(() => { - closeModal(); - setOnHoldModalActive(false); - }, [closeModal]); - - const methodReturn = useMethod('livechat:returnAsInquiry'); - - const handleMoveChat = useCallback(async () => { - try { - await methodReturn(rid); - closeModal(); - Session.set('openedRoom', null); - route.push(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }, [closeModal, dispatchToastMessage, methodReturn, rid, route]); - const requestTranscript = useMethod('livechat:requestTranscript'); const handleRequestTranscript = useCallback( @@ -168,13 +152,13 @@ export const useQuickActions = ( throw new Error(departmentId ? t('error-no-agents-online-in-department') : t('error-forwarding-chat')); } dispatchToastMessage({ type: 'success', message: t('Transferred') }); - route.push(); + homeRoute.push(); closeModal(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeModal, dispatchToastMessage, forwardChat, rid, route, t], + [closeModal, dispatchToastMessage, forwardChat, rid, homeRoute, t], ); const closeChat = useMethod('livechat:closeRoom'); @@ -192,22 +176,42 @@ export const useQuickActions = ( [closeChat, closeModal, dispatchToastMessage, rid, t], ); - const onHoldChat = useEndpoint('POST', '/v1/livechat/room.onHold'); - - const handleOnHoldChat = useCallback(async () => { - try { - await onHoldChat({ roomId: rid }); + const returnChatToQueueMutation = useReturnChatToQueueMutation({ + onSuccess: () => { + Session.set('openedRoom', null); + homeRoute.push(); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { closeModal(); + }, + }); + + const putChatOnHoldMutation = usePutChatOnHoldMutation({ + onSuccess: () => { dispatchToastMessage({ type: 'success', message: t('Chat_On_Hold_Successfully') }); - } catch (error) { + }, + onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); - } - }, [onHoldChat, rid, closeModal, dispatchToastMessage, t]); + }, + onSettled: () => { + closeModal(); + }, + }); const openModal = useMutableCallback(async (id: string) => { switch (id) { case QuickActionsEnum.MoveQueue: - setModal(<ReturnChatQueueModal onMoveChat={handleMoveChat} onCancel={closeModal} />); + setModal( + <ReturnChatQueueModal + onMoveChat={(): void => returnChatToQueueMutation.mutate(rid)} + onCancel={(): void => { + closeModal(); + }} + />, + ); break; case QuickActionsEnum.Transcript: const visitorEmail = await getVisitorEmail(); @@ -241,7 +245,15 @@ export const useQuickActions = ( ); break; case QuickActionsEnum.OnHoldChat: - setModal(<PlaceChatOnHoldModal onOnHoldChat={handleOnHoldChat} onCancel={closeOnHoldModal} />); + setModal( + <PlaceChatOnHoldModal + onOnHoldChat={(): void => putChatOnHoldMutation.mutate(rid)} + onCancel={(): void => { + closeModal(); + setOnHoldModalActive(false); + }} + />, + ); setOnHoldModalActive(true); break; default: diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts new file mode 100644 index 00000000000..022fe5ecb02 --- /dev/null +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts @@ -0,0 +1,26 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useMethod } from '@rocket.chat/ui-contexts'; +import { useMutation, UseMutationOptions, UseMutationResult, useQueryClient } from '@tanstack/react-query'; + +export const useReturnChatToQueueMutation = ( + options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>, +): UseMutationResult<void, Error, IRoom['_id']> => { + const returnChatToQueue = useMethod('livechat:returnAsInquiry'); + + const queryClient = useQueryClient(); + + return useMutation( + async (rid) => { + await returnChatToQueue(rid); + }, + { + ...options, + onSuccess: async (data, rid, context) => { + await queryClient.invalidateQueries(['current-chats']); + queryClient.removeQueries(['rooms', rid]); + queryClient.removeQueries(['subscriptions', { rid }]); + return options?.onSuccess?.(data, rid, context); + }, + }, + ); +}; diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 7ce0d4fa656..1c65aa60630 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -160,7 +160,7 @@ const RoomBody = (): ReactElement => { }); if (!leaderRoomRole) { - return; + return null; } const leaderUser = users.findOne({ _id: leaderRoomRole.u._id }, { fields: { status: 1, statusText: 1 } }); diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx index adb8f9d18f2..477d765347c 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerContainer.tsx @@ -6,7 +6,7 @@ import { ComposerAnonymous } from './ComposerAnonymous'; import { ComposerBlocked } from './ComposerBlocked'; import { ComposerJoinWithPassword } from './ComposerJoinWithPassword'; import ComposerMessage, { ComposerMessageProps } from './ComposerMessage'; -import { ComposerOmnichannel } from './ComposerOmnichannel/ComposerOmnichannel'; +import ComposerOmnichannel from './ComposerOmnichannel/ComposerOmnichannel'; import { ComposerReadOnly } from './ComposerReadOnly'; import ComposerVoIP from './ComposerVoIP'; import { useMessageComposerIsAnonymous } from './hooks/useMessageComposerIsAnonymous'; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx index 3c4043428e2..9b67da7b7b6 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannel.tsx @@ -9,7 +9,7 @@ import { ComposerOmnichannelInquiry } from './ComposerOmnichannelInquiry'; import { ComposerOmnichannelJoin } from './ComposerOmnichannelJoin'; import { ComposerOmnichannelOnHold } from './ComposerOmnichannelOnHold'; -export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement => { +const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement => { const { queuedAt, servedBy, _id, open, onHold } = useOmnichannelRoom(); const isSubscribed = useUserIsSubscribed(); @@ -20,12 +20,14 @@ export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement = const t = useTranslation(); - useEffect(() => { - subscribeToRoom(_id, (entry: IOmnichannelRoom) => { - setIsInquired(!entry.servedBy && entry.queuedAt); - setIsOpen(entry.open); - }); - }, [_id, subscribeToRoom]); + useEffect( + () => + subscribeToRoom(_id, (entry: IOmnichannelRoom) => { + setIsInquired(!entry.servedBy && entry.queuedAt); + setIsOpen(entry.open); + }), + [_id, subscribeToRoom], + ); useEffect(() => { setIsInquired(!servedBy && queuedAt); @@ -57,3 +59,5 @@ export const ComposerOmnichannel = (props: ComposerMessageProps): ReactElement = </> ); }; + +export default ComposerOmnichannel; diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx index f8c74997903..cbac6477676 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelOnHold.tsx @@ -1,18 +1,25 @@ import { MessageFooterCallout, MessageFooterCalloutAction, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; +import { useResumeChatOnHoldMutation } from './hooks/useResumeChatOnHoldMutation'; export const ComposerOmnichannelOnHold = (): ReactElement => { - const resume = useMethod('livechat:resumeOnHold'); + const resumeChatOnHoldMutation = useResumeChatOnHoldMutation(); + const room = useOmnichannelRoom(); + const t = useTranslation(); + return ( <footer className='rc-message-box footer'> <MessageFooterCallout> <MessageFooterCalloutContent>{t('chat_on_hold_due_to_inactivity')}</MessageFooterCalloutContent> - <MessageFooterCalloutAction onClick={(): Promise<unknown> => resume(room._id, { clientAction: true })}> + <MessageFooterCalloutAction + disabled={resumeChatOnHoldMutation.isLoading} + onClick={(): void => resumeChatOnHoldMutation.mutate(room._id)} + > {t('Resume')} </MessageFooterCalloutAction> </MessageFooterCallout> diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts new file mode 100644 index 00000000000..0b76211b6c8 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/hooks/useResumeChatOnHoldMutation.ts @@ -0,0 +1,26 @@ +import { IRoom } from '@rocket.chat/core-typings'; +import { useMethod } from '@rocket.chat/ui-contexts'; +import { useMutation, UseMutationOptions, UseMutationResult, useQueryClient } from '@tanstack/react-query'; + +export const useResumeChatOnHoldMutation = ( + options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>, +): UseMutationResult<void, Error, IRoom['_id']> => { + const resumeChatOnHold = useMethod('livechat:resumeOnHold'); + + const queryClient = useQueryClient(); + + return useMutation( + async (rid) => { + await resumeChatOnHold(rid, { clientAction: true }); + }, + { + ...options, + onSuccess: async (data, rid, context) => { + await queryClient.invalidateQueries(['current-chats']); + await queryClient.invalidateQueries(['rooms', rid]); + await queryClient.invalidateQueries(['subscriptions', { rid }]); + return options?.onSuccess?.(data, rid, context); + }, + }, + ); +}; diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index e2813389ce8..f7d8ddd51e9 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,4 +1,5 @@ import { IRoom } from '@rocket.chat/core-typings'; +import { useRoute } from '@rocket.chat/ui-contexts'; import React, { ReactNode, useMemo, memo, useEffect, ContextType, ReactElement, useCallback } from 'react'; import { UserAction } from '../../../../app/ui'; @@ -7,6 +8,7 @@ import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { RoomManager } from '../../../lib/RoomManager'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; import { useRoomRolesManagement } from '../components/body/useRoomRolesManagement'; import { RoomAPIContext } from '../contexts/RoomAPIContext'; @@ -21,8 +23,17 @@ type RoomProviderProps = { const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useRoomRolesManagement(rid); - const roomQuery = useReactiveQuery(['rooms', rid], ({ rooms }) => rooms.findOne({ _id: rid })); - const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], ({ subscriptions }) => subscriptions.findOne({ rid })); + const roomQuery = useReactiveQuery(['rooms', rid], ({ rooms }) => rooms.findOne({ _id: rid }) ?? null); + + // TODO: the following effect is a workaround while we don't have a general and definitive solution for it + const homeRoute = useRoute('home'); + useEffect(() => { + if (roomQuery.isSuccess && !roomQuery.data) { + homeRoute.push(); + } + }, [roomQuery.isSuccess, roomQuery.data, homeRoute]); + + const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], ({ subscriptions }) => subscriptions.findOne({ rid }) ?? null); const pseudoRoom = useMemo(() => { if (!roomQuery.data) { @@ -56,7 +67,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { return { rid, room: pseudoRoom, - subscription: subscriptionQuery.data, + subscription: subscriptionQuery.data ?? undefined, hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages, @@ -84,7 +95,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { const api = useMemo(() => ({}), []); if (!pseudoRoom) { - return <RoomSkeleton />; + return roomQuery.isSuccess ? <RoomNotFound /> : <RoomSkeleton />; } return ( diff --git a/apps/meteor/definition/methods/omnichannel.ts b/apps/meteor/definition/methods/omnichannel.ts index 6207de603d1..3a2f1a30c22 100644 --- a/apps/meteor/definition/methods/omnichannel.ts +++ b/apps/meteor/definition/methods/omnichannel.ts @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, IRoom } from '@rocket.chat/core-typings'; import '@rocket.chat/ui-contexts'; declare module '@rocket.chat/ui-contexts' { @@ -36,7 +36,7 @@ declare module '@rocket.chat/ui-contexts' { 'livechat:removeTrigger': (...args: any[]) => any; 'livechat:removeUnit': (...args: any[]) => any; 'livechat:requestTranscript': (...args: any[]) => any; - 'livechat:returnAsInquiry': (...args: any[]) => any; + 'livechat:returnAsInquiry': (rid: IRoom['_id'], departmentID?: ILivechatDepartment['_id']) => boolean; 'livechat:sendTranscript': (...args: any[]) => any; 'livechat:transfer': (...args: any[]) => any; 'livechat:saveAppearance': (...args: any[]) => any; -- GitLab From 67d5339d6afdb04f7930a0cb88540eac59e6a7a0 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Tue, 4 Oct 2022 01:54:45 -0300 Subject: [PATCH 091/107] Regression: Fixed takeInquiry method not displaying error messages on the client (#26976) --- .../composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx | 2 +- packages/ui-contexts/src/ServerContext/methods.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx index a0400c2795a..5679fd79a9e 100644 --- a/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx +++ b/apps/meteor/client/views/room/components/body/composer/ComposerOmnichannel/ComposerOmnichannelInquiry.tsx @@ -25,7 +25,7 @@ export const ComposerOmnichannelInquiry = (): ReactElement => { return; } try { - await takeInquiry(result.data.inquiry._id); + await takeInquiry(result.data.inquiry._id, { clientAction: true }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 172008d9abd..d3073fc4c6e 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -226,7 +226,7 @@ export interface ServerMethods { 'checkRegistrationSecretURL': (hash: string) => boolean; 'livechat:changeLivechatStatus': (params?: void | { status?: string; agentId?: string }) => unknown; 'livechat:saveAgentInfo': (_id: string, agentData: unknown, agentDepartments: unknown) => unknown; - 'livechat:takeInquiry': (inquiryId: string) => unknown; + 'livechat:takeInquiry': (inquiryId: string, options?: { clientAction: boolean; forwardingToDepartment?: boolean }) => unknown; 'livechat:resumeOnHold': (roomId: string, options?: { clientAction: boolean }) => unknown; 'autoTranslate.getProviderUiMetadata': () => Record<string, { name: string; displayName: string }>; 'autoTranslate.getSupportedLanguages': (language: string) => ISupportedLanguage[]; -- GitLab From 0c1ab59d7efb316cd315bd12c98da1633ecdaf36 Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Tue, 4 Oct 2022 10:12:10 -0300 Subject: [PATCH 092/107] [IMPROVE] Use cached EE Cloud license on startup (#26987) --- .../server/functions/connectWorkspace.ts | 13 ++++---- .../functions/finishOAuthAuthorization.js | 13 ++++---- .../server/functions/getConfirmationPoll.ts | 16 ++++----- .../functions/getUserCloudAccessToken.js | 17 ++++++---- .../getWorkspaceAccessTokenWithScope.js | 20 ++++++----- .../server/functions/getWorkspaceLicense.ts | 33 ++++++++++++------- .../functions/startRegisterWorkspace.ts | 14 ++++---- .../startRegisterWorkspaceSetupWizard.ts | 15 +++++---- .../cloud/server/functions/syncWorkspace.ts | 17 +++++----- .../app/cloud/server/functions/userLogout.js | 13 ++++---- 10 files changed, 96 insertions(+), 75 deletions(-) diff --git a/apps/meteor/app/cloud/server/functions/connectWorkspace.ts b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts index b3b0c095715..15ea5adfcc2 100644 --- a/apps/meteor/app/cloud/server/functions/connectWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts @@ -36,12 +36,13 @@ export async function connectWorkspace(token: string) { }, data: regInfo, }); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to register with Rocket.Chat Cloud. Error: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to Connect with Rocket.Chat Cloud', + url: '/api/oauth/clients', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); return false; } diff --git a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js index e46bfda5cfd..e9cae29053c 100644 --- a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js +++ b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js @@ -33,12 +33,13 @@ export function finishOAuthAuthorization(code, state) { redirect_uri: getRedirectUri(), }, }); - } catch (e) { - if (e.response && e.response.data && e.response.data.error) { - SystemLogger.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err) { + SystemLogger.error({ + msg: 'Failed to finish OAuth authorization with Rocket.Chat Cloud', + url: '/api/oauth/token', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); return false; } diff --git a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts index 4405d0ba33a..75ef05688fc 100644 --- a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts +++ b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts @@ -10,18 +10,18 @@ export async function getConfirmationPoll(deviceCode: string): Promise<CloudConf let result; try { result = HTTP.get(`${cloudUrl}/api/v2/register/workspace/poll?token=${deviceCode}`); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to get confirmation poll from Rocket.Chat Cloud', + url: '/api/v2/register/workspace/poll', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); - throw e; + throw err; } const { data } = result; - if (!data) { throw new Error('Failed to retrieve registration confirmation poll data'); } diff --git a/apps/meteor/app/cloud/server/functions/getUserCloudAccessToken.js b/apps/meteor/app/cloud/server/functions/getUserCloudAccessToken.js index b187c48d690..0453a299f10 100644 --- a/apps/meteor/app/cloud/server/functions/getUserCloudAccessToken.js +++ b/apps/meteor/app/cloud/server/functions/getUserCloudAccessToken.js @@ -61,20 +61,23 @@ export function getUserCloudAccessToken(userId, forceNew = false, scope = '', sa redirect_uri: redirectUri, }, }); - } catch (e) { - if (e.response && e.response.data && e.response.data.error) { - SystemLogger.error(`Failed to get User AccessToken from Rocket.Chat Cloud. Error: ${e.response.data.error}`); + } catch (err) { + SystemLogger.error({ + msg: 'Failed to get User AccessToken from Rocket.Chat Cloud', + url: '/api/oauth/token', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); - if (e.response.data.error === 'oauth_invalid_client_credentials') { + if (err.response?.data?.error) { + if (err.response.data.error === 'oauth_invalid_client_credentials') { SystemLogger.error('Server has been unregistered from cloud'); unregisterWorkspace(); } - if (e.response.data.error === 'unauthorized') { + if (err.response.data.error === 'unauthorized') { userLoggedOut(userId); } - } else { - SystemLogger.error(e); } return ''; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js index 7bcdb01c92d..19a7bce57f4 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js @@ -41,16 +41,18 @@ export function getWorkspaceAccessTokenWithScope(scope = '') { redirect_uri: redirectUri, }, }); - } catch (e) { - if (e.response && e.response.data && e.response.data.error) { - SystemLogger.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${e.response.data.error}`); + } catch (err) { + SystemLogger.error({ + msg: 'Failed to get Workspace AccessToken from Rocket.Chat Cloud', + url: '/api/oauth/token', + scope, + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); - if (e.response.data.error === 'oauth_invalid_client_credentials') { - SystemLogger.error('Server has been unregistered from cloud'); - unregisterWorkspace(); - } - } else { - SystemLogger.error(e); + if (err.response?.data?.error === 'oauth_invalid_client_credentials') { + SystemLogger.error('Server has been unregistered from cloud'); + unregisterWorkspace(); } return tokenResponse; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index ac556eb33cf..584990454fa 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -7,11 +7,21 @@ import { callbacks } from '../../../../lib/callbacks'; import { LICENSE_VERSION } from '../license'; import { SystemLogger } from '../../../../server/lib/logger/system'; -export async function getWorkspaceLicense() { - const token = await getWorkspaceAccessToken(); +export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> { + const currentLicense = await Settings.findOne('Cloud_Workspace_License'); + + const cachedLicenseReturn = () => { + const license = currentLicense?.value as string; + if (license) { + callbacks.run('workspaceLicenseChanged', license); + } + + return { updated: false, license }; + }; + const token = await getWorkspaceAccessToken(); if (!token) { - return { updated: false, license: '' }; + return cachedLicenseReturn(); } let licenseResult; @@ -21,25 +31,24 @@ export async function getWorkspaceLicense() { Authorization: `Bearer ${token}`, }, }); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to update license from Rocket.Chat Cloud. Error: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to update license from Rocket.Chat Cloud', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); - return { updated: false, license: '' }; + return cachedLicenseReturn(); } const remoteLicense = licenseResult.data; - const currentLicense = await Settings.findOne('Cloud_Workspace_License'); if (!currentLicense || !currentLicense._updatedAt) { throw new Error('Failed to retrieve current license'); } if (remoteLicense.updatedAt <= currentLicense._updatedAt) { - return { updated: false, license: '' }; + return cachedLicenseReturn(); } await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index a3a19810caf..4d73fd3d7c7 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -26,12 +26,14 @@ export async function startRegisterWorkspace(resend = false) { result = HTTP.post(`${cloudUrl}/api/v2/register/workspace?resend=${resend}`, { data: regInfo, }); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to register with Rocket.Chat Cloud', + url: '/api/v2/register/workspace', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); + return false; } const { data } = result; diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index d0aa9e77045..bb17647772e 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -14,14 +14,15 @@ export async function startRegisterWorkspaceSetupWizard(resend = false, email: s result = HTTP.post(`${cloudUrl}/api/v2/register/workspace/intent?resent=${resend}`, { data: regInfo, }); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to register workspace intent with Rocket.Chat Cloud', + url: '/api/v2/register/workspace', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); - throw e; + throw err; } const { data } = result; diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts index 72f0a2eb335..56446e7d60c 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts @@ -35,16 +35,17 @@ export async function syncWorkspace(reconnectCheck = false) { data: info, headers, }); - - await getWorkspaceLicense(); - } catch (e: any) { - if (e.response?.data?.error) { - SystemLogger.error(`Failed to sync with Rocket.Chat Cloud. Error: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); return false; + } finally { + // aways fetch the license + await getWorkspaceLicense(); } const { data } = result; diff --git a/apps/meteor/app/cloud/server/functions/userLogout.js b/apps/meteor/app/cloud/server/functions/userLogout.js index 6d2e917f347..5e133ac3e32 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.js +++ b/apps/meteor/app/cloud/server/functions/userLogout.js @@ -40,12 +40,13 @@ export function userLogout(userId) { token_type_hint: 'refresh_token', }, }); - } catch (e) { - if (e.response && e.response.data && e.response.data.error) { - SystemLogger.error(`Failed to get Revoke refresh token to logout of Rocket.Chat Cloud. Error: ${e.response.data.error}`); - } else { - SystemLogger.error(e); - } + } catch (err) { + SystemLogger.error({ + msg: 'Failed to get Revoke refresh token to logout of Rocket.Chat Cloud', + url: '/api/oauth/revoke', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); } } -- GitLab From 3c96bc72946e8c7ce5458945e984479c0d068ff3 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat> Date: Tue, 4 Oct 2022 10:23:50 -0300 Subject: [PATCH 093/107] Regression: Omnichannel Contact Center empty on no filter search (#26975) Co-authored-by: Kevin Aleman <kaleman960@gmail.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../livechat/imports/server/rest/visitors.ts | 2 +- .../api/lib/{visitors.js => visitors.ts} | 50 ++++++++++++++++--- .../server/models/raw/LivechatVisitors.ts | 43 ++++++++++------ .../end-to-end/api/livechat/09-visitors.ts | 47 +++++++++++++++-- .../src/models/ILivechatVisitorsModel.ts | 6 +-- 5 files changed, 118 insertions(+), 30 deletions(-) rename apps/meteor/app/livechat/server/api/lib/{visitors.js => visitors.ts} (70%) diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.ts b/apps/meteor/app/livechat/imports/server/rest/visitors.ts index 69e99086e37..43dff642c4b 100644 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.ts +++ b/apps/meteor/app/livechat/imports/server/rest/visitors.ts @@ -139,7 +139,7 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); - const nameOrUsername = term && new RegExp(escapeRegExp(term), 'i'); + const nameOrUsername = term ? new RegExp(escapeRegExp(term), 'i') : undefined; return API.v1.success( await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.js b/apps/meteor/app/livechat/server/api/lib/visitors.ts similarity index 70% rename from apps/meteor/app/livechat/server/api/lib/visitors.js rename to apps/meteor/app/livechat/server/api/lib/visitors.ts index b16bb4eeea4..23bbb792ba6 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.js +++ b/apps/meteor/app/livechat/server/api/lib/visitors.ts @@ -1,8 +1,10 @@ +import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser, IVisitor } from '@rocket.chat/core-typings'; import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models'; +import type { FindOptions } from 'mongodb'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; -export async function findVisitorInfo({ visitorId }) { +export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { const visitor = await LivechatVisitors.findOneById(visitorId); if (!visitor) { throw new Error('visitor-not-found'); @@ -13,7 +15,13 @@ export async function findVisitorInfo({ visitorId }) { }; } -export async function findVisitedPages({ roomId, pagination: { offset, count, sort } }) { +export async function findVisitedPages({ + roomId, + pagination: { offset, count, sort }, +}: { + roomId: IRoom['_id']; + pagination: { offset: number; count: number; sort: FindOptions<IMessage>['sort'] }; +}) { const room = await LivechatRooms.findOneById(roomId); if (!room) { throw new Error('invalid-room'); @@ -34,7 +42,17 @@ export async function findVisitedPages({ roomId, pagination: { offset, count, so }; } -export async function findChatHistory({ userId, roomId, visitorId, pagination: { offset, count, sort } }) { +export async function findChatHistory({ + userId, + roomId, + visitorId, + pagination: { offset, count, sort }, +}: { + userId: IUser['_id']; + roomId: IRoom['_id']; + visitorId: IVisitor['_id']; + pagination: { offset: number; count: number; sort: FindOptions<IOmnichannelRoom>['sort'] }; +}) { const room = await LivechatRooms.findOneById(roomId); if (!room) { throw new Error('invalid-room'); @@ -67,6 +85,14 @@ export async function searchChats({ closedChatsOnly, servedChatsOnly: served, pagination: { offset, count, sort }, +}: { + userId: IUser['_id']; + roomId: IRoom['_id']; + visitorId: IVisitor['_id']; + searchText?: string; + closedChatsOnly?: string; + servedChatsOnly?: string; + pagination: { offset: number; count: number; sort: FindOptions<IOmnichannelRoom>['sort'] }; }) { const room = await LivechatRooms.findOneById(roomId); if (!room) { @@ -104,14 +130,22 @@ export async function searchChats({ history, count: history.length, offset, - total: (total && total.count) || 0, + total: total?.count ?? 0, }; } -export async function findVisitorsToAutocomplete({ selector }) { +export async function findVisitorsToAutocomplete({ + selector, +}: { + selector: { + exceptions?: ILivechatVisitor['_id'][]; + conditions?: Record<string, unknown>; + term: string; + }; +}) { const { exceptions = [], conditions = {} } = selector; - const options = { + const options: FindOptions<ILivechatVisitor> = { projection: { _id: 1, name: 1, @@ -133,6 +167,10 @@ export async function findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ emailOrPhone, nameOrUsername, pagination: { offset, count, sort }, +}: { + emailOrPhone?: string; + nameOrUsername?: RegExp; + pagination: { offset: number; count: number; sort: FindOptions<IVisitor>['sort'] }; }) { const allowedCF = await LivechatCustomField.findMatchingCustomFields('visitor', true, { projection: { _id: 1 } }) .map((cf) => cf._id) diff --git a/apps/meteor/server/models/raw/LivechatVisitors.ts b/apps/meteor/server/models/raw/LivechatVisitors.ts index 8a1fc40eec7..7e4b88e712f 100644 --- a/apps/meteor/server/models/raw/LivechatVisitors.ts +++ b/apps/meteor/server/models/raw/LivechatVisitors.ts @@ -160,26 +160,37 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL * Find visitors by their email or phone or username or name */ async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( - emailOrPhone: string, - nameOrUsername: RegExp, + emailOrPhone?: string, + nameOrUsername?: RegExp, allowedCustomFields: string[] = [], options?: FindOptions<ILivechatVisitor>, ): Promise<FindPaginated<FindCursor<ILivechatVisitor>>> { - const query = { + if (!emailOrPhone && !nameOrUsername && allowedCustomFields.length === 0) { + return this.findPaginated({}, options); + } + + const query: Filter<ILivechatVisitor> = { $or: [ - { - 'visitorEmails.address': emailOrPhone, - }, - { - 'phone.phoneNumber': emailOrPhone, - }, - { - name: nameOrUsername, - }, - { - username: nameOrUsername, - }, - // nameorusername is a clean regex, so we should be good + ...(emailOrPhone + ? [ + { + 'visitorEmails.address': emailOrPhone, + }, + { + 'phone.phoneNumber': emailOrPhone, + }, + ] + : []), + ...(nameOrUsername + ? [ + { + name: nameOrUsername, + }, + { + username: nameOrUsername, + }, + ] + : []), ...allowedCustomFields.map((c: string) => ({ [`livechatData.${c}`]: nameOrUsername })), ], }; diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index d8aaa9f7670..0a5cfdf2fbb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -793,8 +793,47 @@ describe('LIVECHAT - visitors', function () { expect(res.body.room.v._id).to.equal(visitor2._id); }); }); -}); + describe('livechat/visitors.search', () => { + it('should fail if user doesnt have view-l-room permission', async () => { + await updatePermission('view-l-room', []); + const res = await request.get(api(`livechat/visitors.search?text=nel`)).set(credentials).send(); + expect(res.body).to.have.property('success', false); + }); + it('should fail if term is not on query params', async () => { + await updatePermission('view-l-room', ['admin', 'livechat-agent']); + const res = await request.get(api(`livechat/visitors.search`)).set(credentials).send(); + expect(res.body).to.have.property('success', false); + }); + it('should not fail when term is an evil regex string', async () => { + const res = await request.get(api(`livechat/visitors.search?term=^((ab)*)+$`)).set(credentials).send(); + expect(res.body).to.have.property('success', true); + }); + it('should return a list of visitors when term is a valid string', async () => { + const visitor = await createVisitor(); -// TODO: Missing tests for the following endpoints: -// - /v1/livechat/visitor.status -// - /v1/livechat/visitor.callStatus + const res = await request + .get(api(`livechat/visitors.search?term=${visitor.name}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.visitors).to.be.an('array'); + expect(res.body.visitors).to.have.lengthOf.greaterThan(0); + expect(res.body.visitors[0]).to.have.property('_id', visitor._id); + expect(res.body.visitors[0]).to.have.property('name', visitor.name); + expect(res.body.visitors[0]).to.have.property('username', visitor.username); + expect(res.body.visitors[0]).to.have.property('visitorEmails'); + expect(res.body.visitors[0]).to.have.property('phone'); + }); + it('should return a list of visitors when term is an empty string', async () => { + const res = await request.get(api(`livechat/visitors.search?term=`)).set(credentials).send(); + expect(res.body).to.have.property('success', true); + expect(res.body.visitors).to.be.an('array'); + expect(res.body.visitors).to.have.lengthOf.greaterThan(0); + expect(res.body.visitors[0]).to.have.property('_id'); + expect(res.body.visitors[0]).to.have.property('username'); + expect(res.body.visitors[0]).to.have.property('name'); + expect(res.body.visitors[0]).to.have.property('phone'); + expect(res.body.visitors[0]).to.have.property('visitorEmails'); + }); + }); +}); diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index 4e381ce5566..ace43ca6b5d 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -18,9 +18,9 @@ export interface ILivechatVisitorsModel extends IBaseModel<ILivechatVisitor> { } >; findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( - emailOrPhone: string, - nameOrUsername: RegExp, - allowedCustomFields: string[], + emailOrPhone?: string, + nameOrUsername?: RegExp, + allowedCustomFields?: string[], options?: FindOptions<ILivechatVisitor>, ): Promise<FindPaginated<FindCursor<ILivechatVisitor>>>; -- GitLab From 175d6a1b33528b83de0d068100df07a0b1c606cb Mon Sep 17 00:00:00 2001 From: Filipe Marins <filipe.marins@rocket.chat> Date: Tue, 4 Oct 2022 12:40:28 -0300 Subject: [PATCH 094/107] Regression: Remove section and replace icon on administration kebab menu (#26986) * refactor: kebab menu update * chore: update package and icon * chore: update fuselage Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- apps/meteor/app/livechat/client/ui.js | 2 +- .../AdministrationList/AdministrationList.tsx | 5 - .../AdministrationModelList.tsx | 2 +- .../AdministrationList/AppsModelList.tsx | 2 +- .../AdministrationList/SettingsModelList.tsx | 32 ----- .../sidebar/header/actions/Administration.tsx | 6 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 +- .../tests/e2e/administration-menu.spec.ts | 10 +- .../AdministrationList.spec.tsx | 6 - .../AdministrationModelList.spec.tsx | 8 +- .../SettingsModelList.spec.tsx | 32 ----- yarn.lock | 131 ++++++++++++++++-- 12 files changed, 128 insertions(+), 110 deletions(-) delete mode 100644 apps/meteor/client/components/AdministrationList/SettingsModelList.tsx delete mode 100644 apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx diff --git a/apps/meteor/app/livechat/client/ui.js b/apps/meteor/app/livechat/client/ui.js index 742ffd8ad7c..6b7eade52a1 100644 --- a/apps/meteor/app/livechat/client/ui.js +++ b/apps/meteor/app/livechat/client/ui.js @@ -14,7 +14,7 @@ Tracker.autorun((c) => { }); AccountBox.addItem({ - name: 'Manage_Omnichannel', + name: 'Omnichannel', icon: 'headset', href: '/omnichannel/current', sideNav: 'omnichannelFlex', diff --git a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx index e9a570ffd21..468a78f6d93 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx @@ -5,7 +5,6 @@ import { AccountBoxItem, IAppAccountBoxItem, isAppAccountBoxItem } from '../../. import AdministrationModelList from './AdministrationModelList'; import AppsModelList from './AppsModelList'; import AuditModelList from './AuditModelList'; -import SettingsModelList from './SettingsModelList'; type AdministrationListProps = { accountBoxItems: (IAppAccountBoxItem | AccountBoxItem)[]; @@ -15,7 +14,6 @@ type AdministrationListProps = { hasAuditPermission: boolean; hasAuditLogPermission: boolean; hasManageApps: boolean; - hasSettingsPermission: boolean; }; const AdministrationList: FC<AdministrationListProps> = ({ @@ -23,7 +21,6 @@ const AdministrationList: FC<AdministrationListProps> = ({ hasAuditPermission, hasAuditLogPermission, hasManageApps, - hasSettingsPermission, hasAdminPermission, closeList, }) => { @@ -32,11 +29,9 @@ const AdministrationList: FC<AdministrationListProps> = ({ const showAudit = hasAuditPermission || hasAuditLogPermission; const showManageApps = hasManageApps || !!appBoxItems.length; const showAdmin = hasAdminPermission || !!adminBoxItems.length; - const showSettings = hasSettingsPermission; const list = [ showAdmin && <AdministrationModelList showAdmin={showAdmin} accountBoxItems={adminBoxItems} closeList={closeList} />, - showSettings && <SettingsModelList closeList={closeList} />, showManageApps && <AppsModelList appBoxItems={appBoxItems} closeList={closeList} showManageApps={showManageApps} />, showAudit && <AuditModelList showAudit={hasAuditPermission} showAuditLog={hasAuditLogPermission} closeList={closeList} />, ]; diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx index 312972d5aee..b2f03fd3941 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx @@ -53,7 +53,7 @@ const AdministrationModelList: FC<AdministrationModelListProps> = ({ accountBoxI )} <ListItem icon='cog' - text={t('Manage_workspace')} + text={t('Workspace')} action={(): void => { if (hasInfoPermission) { infoRoute.push(); diff --git a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx index 6050ff5d1e3..3f9291fe2cc 100644 --- a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx @@ -23,7 +23,7 @@ const AppsModelList: FC<AppsModelListProps> = ({ appBoxItems, showManageApps, cl {showManageApps && ( <> <ListItem - icon='cube' + icon='store' text={t('Marketplace')} action={(): void => { marketplaceRoute.push(); diff --git a/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx b/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx deleted file mode 100644 index 245640aa119..00000000000 --- a/apps/meteor/client/components/AdministrationList/SettingsModelList.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { OptionTitle } from '@rocket.chat/fuselage'; -import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; - -import ListItem from '../Sidebar/ListItem'; - -type SettingsModelListProps = { - closeList: () => void; -}; - -const SettingsModelList: FC<SettingsModelListProps> = ({ closeList }) => { - const t = useTranslation(); - const settingsRoute = useRoute('admin-settings'); - - return ( - <> - <OptionTitle>{t('Settings')}</OptionTitle> - <ul> - <ListItem - icon='customize' - text={t('Workspace_settings')} - action={(): void => { - settingsRoute.push(); - closeList(); - }} - /> - </ul> - </> - ); -}; - -export default SettingsModelList; diff --git a/apps/meteor/client/sidebar/header/actions/Administration.tsx b/apps/meteor/client/sidebar/header/actions/Administration.tsx index 3669dde3ac3..d0fbcd5fa12 100644 --- a/apps/meteor/client/sidebar/header/actions/Administration.tsx +++ b/apps/meteor/client/sidebar/header/actions/Administration.tsx @@ -27,7 +27,6 @@ const ADMIN_PERMISSIONS = [ 'manage-own-incoming-integrations', 'view-engagement-dashboard', ]; -const SETTINGS_PERMISSIONS = ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']; const AUDIT_PERMISSIONS = ['can-audit']; const AUDIT_LOG_PERMISSIONS = ['can-audit-log']; const MANAGE_APPS_PERMISSIONS = ['manage-apps']; @@ -48,9 +47,7 @@ const Administration: VFC<Omit<HTMLAttributes<HTMLElement>, 'is'>> = (props) => const hasAuditLogPermission = useAtLeastOnePermission(AUDIT_LOG_PERMISSIONS) && hasAuditLicense; const hasManageApps = useAtLeastOnePermission(MANAGE_APPS_PERMISSIONS); const hasAdminPermission = useAtLeastOnePermission(ADMIN_PERMISSIONS); - const hasSettingsPermission = useAtLeastOnePermission(SETTINGS_PERMISSIONS); - const showMenu = - hasAuditPermission || hasAuditLogPermission || hasManageApps || hasAdminPermission || hasSettingsPermission || !!accountBoxItems.length; + const showMenu = hasAuditPermission || hasAuditLogPermission || hasManageApps || hasAdminPermission || !!accountBoxItems.length; return ( <> @@ -66,7 +63,6 @@ const Administration: VFC<Omit<HTMLAttributes<HTMLElement>, 'is'>> = (props) => hasAuditPermission={hasAuditPermission} hasAuditLogPermission={hasAuditLogPermission} hasManageApps={hasManageApps} - hasSettingsPermission={hasSettingsPermission} /> </Dropdown>, document.body, diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 965b5b33848..bab13b264e5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5302,7 +5302,7 @@ "Would_you_like_to_place_chat_on_hold": "Would you like to place this chat On-Hold?", "Wrap_up_the_call": "Wrap-up the call", "Wrap_Up_Notes": "Wrap-Up Notes", - "Workspace_settings": "Workspace settings", + "Workspace": "Workspace", "Yes": "Yes", "Yes_archive_it": "Yes, archive it!", "Yes_clear_all": "Yes, clear all!", diff --git a/apps/meteor/tests/e2e/administration-menu.spec.ts b/apps/meteor/tests/e2e/administration-menu.spec.ts index 701965e12f7..4da0bf93361 100644 --- a/apps/meteor/tests/e2e/administration-menu.spec.ts +++ b/apps/meteor/tests/e2e/administration-menu.spec.ts @@ -21,23 +21,17 @@ test.describe.serial('administration-menu', () => { }); test('expect open info page', async ({ page }) => { - await poHomeDiscussion.sidenav.openAdministrationByLabel('Manage workspace'); + await poHomeDiscussion.sidenav.openAdministrationByLabel('Workspace'); await expect(page).toHaveURL('admin/info'); }); test('expect open omnichannel page', async ({ page }) => { - await poHomeDiscussion.sidenav.openAdministrationByLabel('Manage Omnichannel'); + await poHomeDiscussion.sidenav.openAdministrationByLabel('Omnichannel'); await expect(page).toHaveURL('omnichannel/current'); }); - test('expect open settings page', async ({ page }) => { - await poHomeDiscussion.sidenav.openAdministrationByLabel('Workspace settings'); - - await expect(page).toHaveURL('admin/settings'); - }); - test('expect open app marketplace page', async ({ page }) => { await poHomeDiscussion.sidenav.openAdministrationByLabel('Marketplace'); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx index bde3be57328..dffb65d33b5 100644 --- a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationList.spec.tsx @@ -21,7 +21,6 @@ const defaultConfig = { './AdministrationModelList': () => <p>Administration Model List</p>, './AppsModelList': () => <p>Apps Model List</p>, './AuditModelList': () => <p>Audit Model List</p>, - './SettingsModelList': () => <p>Settings Model List</p>, }; describe('components/AdministrationList/AdministrationList', () => { @@ -34,14 +33,12 @@ describe('components/AdministrationList/AdministrationList', () => { hasAuditPermission={true} hasAuditLogPermission={true} hasManageApps={true} - hasSettingsPermission={true} hasAdminPermission={true} />, ); expect(screen.getByText('Administration Model List')).to.exist; expect(screen.getByText('Apps Model List')).to.exist; - expect(screen.getByText('Settings Model List')).to.exist; expect(screen.getByText('Audit Model List')).to.exist; }); @@ -56,7 +53,6 @@ describe('components/AdministrationList/AdministrationList', () => { expect(screen.queryByText('Administration Model List')).to.not.exist; expect(screen.queryByText('Apps Model List')).to.not.exist; - expect(screen.queryByText('Settings Model List')).to.not.exist; expect(screen.queryByText('Audit Model List')).to.not.exist; }); @@ -71,7 +67,6 @@ describe('components/AdministrationList/AdministrationList', () => { expect(screen.getByText('Administration Model List')).to.exist; expect(screen.queryByText('Apps Model List')).to.not.exist; - expect(screen.queryByText('Settings Model List')).to.not.exist; expect(screen.queryByText('Audit Model List')).to.not.exist; }); @@ -91,7 +86,6 @@ describe('components/AdministrationList/AdministrationList', () => { expect(screen.getByText('Apps Model List')).to.exist; expect(screen.queryByText('Administration Model List')).to.not.exist; - expect(screen.queryByText('Settings Model List')).to.not.exist; expect(screen.queryByText('Audit Model List')).to.not.exist; }); }); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx index cd22ff963c4..a9db32cad70 100644 --- a/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AdministrationModelList.spec.tsx @@ -40,7 +40,7 @@ describe('components/AdministrationList/AdministrationModelList', () => { render(<AdministrationModelList closeList={() => null} accountBoxItems={[]} showAdmin={true} />); expect(screen.getByText('Administration')).to.exist; - expect(screen.getByText('Manage_workspace')).to.exist; + expect(screen.getByText('Workspace')).to.exist; expect(screen.getByText('Upgrade')).to.exist; }); @@ -49,7 +49,7 @@ describe('components/AdministrationList/AdministrationModelList', () => { render(<AdministrationModelList closeList={() => null} accountBoxItems={[]} showAdmin={false} />); expect(screen.getByText('Administration')).to.exist; - expect(screen.queryByText('Manage_workspace')).to.not.exist; + expect(screen.queryByText('Workspace')).to.not.exist; expect(screen.queryByText('Upgrade')).to.not.exist; }); @@ -63,7 +63,7 @@ describe('components/AdministrationList/AdministrationModelList', () => { <AdministrationModelList closeList={closeList} accountBoxItems={[]} showAdmin={true} /> </RouterContextMock>, ); - const button = screen.getByText('Manage_workspace'); + const button = screen.getByText('Workspace'); userEvent.click(button); await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-info')); @@ -85,7 +85,7 @@ describe('components/AdministrationList/AdministrationModelList', () => { <AdministrationModelList closeList={closeList} accountBoxItems={[]} showAdmin={true} /> </RouterContextMock>, ); - const button = screen.getByText('Manage_workspace'); + const button = screen.getByText('Workspace'); userEvent.click(button); await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-index')); diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx deleted file mode 100644 index 5d711b4269a..00000000000 --- a/apps/meteor/tests/unit/client/components/AdministrationList/SettingsModelList.spec.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { expect, spy } from 'chai'; -import React from 'react'; - -import SettingsModelList from '../../../../../client/components/AdministrationList/SettingsModelList'; -import RouterContextMock from '../../../../mocks/client/RouterContextMock'; - -describe('components/AdministrationList/SettingsModelList', () => { - it('should render', async () => { - render(<SettingsModelList closeList={() => null} />); - - expect(screen.getByText('Workspace_settings')).to.exist; - }); - - context('when clicked', () => { - it('should go to admin settings', async () => { - const pushRoute = spy(); - const closeList = spy(); - render( - <RouterContextMock pushRoute={pushRoute}> - <SettingsModelList closeList={closeList} /> - </RouterContextMock>, - ); - const button = screen.getByText('Workspace_settings'); - - userEvent.click(button); - await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-settings')); - await waitFor(() => expect(closeList).to.have.been.called()); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index 9275f5dbbce..2b184725228 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4284,6 +4284,18 @@ __metadata: languageName: node linkType: hard +"@react-stately/data@npm:^3.6.1": + version: 3.6.1 + resolution: "@react-stately/data@npm:3.6.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 79c9eae2ff674453010ed9b37fc23de687fe9dd5413c2931cadd2b7d85bcd567ed45078c0234d1c5117677321ad3eaf34952af6113546033643a7e78dde23242 + languageName: node + linkType: hard + "@react-stately/datepicker@npm:^3.0.2": version: 3.0.2 resolution: "@react-stately/datepicker@npm:3.0.2" @@ -5123,6 +5135,19 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-in-js@npm:~0.31.19-dev.26": + version: 0.31.19-dev.26 + resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.26" + dependencies: + "@emotion/hash": ^0.8.0 + "@rocket.chat/css-supports": ~0.31.19-dev.26 + "@rocket.chat/memo": ~0.31.19-dev.26 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.26 + stylis: ~4.0.13 + checksum: a89128e73179c98d9b78f26cfe1ee26ec07581427b056f2142373a15c56204e3d489f04c50abf35baff83109848c61a1d286320e0dfe683d9ea80c0ffbaba4c4 + languageName: node + linkType: hard + "@rocket.chat/css-supports@npm:~0.31.19-dev.19": version: 0.31.19-dev.19 resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.19" @@ -5132,6 +5157,15 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-supports@npm:~0.31.19-dev.26": + version: 0.31.19-dev.26 + resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.26" + dependencies: + "@rocket.chat/memo": ~0.31.19-dev.26 + checksum: c890f2b8b32f77dba62569e5ca3d5e79e0d50a4b3678334afdaa8213385c1b2a9481033aaab263905abe0c833fb1f26e919f384fba714030fa8a99226e87f3d8 + languageName: node + linkType: hard + "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer": version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" @@ -5319,13 +5353,20 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:next, @rocket.chat/fuselage-tokens@npm:~0.32.0-dev.101": +"@rocket.chat/fuselage-tokens@npm:next": version: 0.32.0-dev.101 resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.101" checksum: 516aeecbfb84311bf819a867447a189467c7058cf65affe1e4b43ebde48b1d7db3714cbbcb4d1e8f4cc45369338f643ecd6d9e3d8b7b076ec12319492e1d8979 languageName: node linkType: hard +"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.108": + version: 0.32.0-dev.108 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.108" + checksum: 40a85e19eea4359c56d16a253b30bcb10c85be3e117e836afeb57bd29596a809d4820278801a0b1dbf6dc8c38279006fcc8cfab761a724de974b6d0e14c057fc + languageName: node + linkType: hard + "@rocket.chat/fuselage-ui-kit@npm:next": version: 0.32.0-dev.104 resolution: "@rocket.chat/fuselage-ui-kit@npm:0.32.0-dev.104" @@ -5391,16 +5432,18 @@ __metadata: linkType: soft "@rocket.chat/fuselage@npm:next": - version: 0.32.0-dev.151 - resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.151" - dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.19 - "@rocket.chat/css-supports": ~0.31.19-dev.19 - "@rocket.chat/fuselage-tokens": ~0.32.0-dev.101 - "@rocket.chat/memo": ~0.31.19-dev.19 - "@rocket.chat/styled": ~0.31.19-dev.19 + version: 0.32.0-dev.158 + resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.158" + dependencies: + "@rocket.chat/css-in-js": ~0.31.19-dev.26 + "@rocket.chat/css-supports": ~0.31.19-dev.26 + "@rocket.chat/fuselage-tokens": ~0.32.0-dev.108 + "@rocket.chat/memo": ~0.31.19-dev.26 + "@rocket.chat/styled": ~0.31.19-dev.26 invariant: ^2.2.4 + react-aria: ~3.19.0 react-keyed-flatten-children: ^1.3.0 + react-stately: ~3.17.0 peerDependencies: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/fuselage-polyfills": "*" @@ -5408,7 +5451,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 9d9d278e47b3e07095d6fb3ff988c1aafa5da802444069d9898b5d6fc706685949a939fdd9f336c6eac6a7d853c83ec15762158a9a11faab3c1a44aedf2e4a77 + checksum: bd617d89fb2477a068bec33421457b93b5f97066c796bd621e8e358608d893fe41cbfaba9f7ebfea3435d98756ed473d805b1bad3d231c7ffc0699b5242fdd55 languageName: node linkType: hard @@ -5474,9 +5517,9 @@ __metadata: linkType: soft "@rocket.chat/icons@npm:next": - version: 0.32.0-dev.130 - resolution: "@rocket.chat/icons@npm:0.32.0-dev.130" - checksum: e7dbbf45e7b7eeb81670746d2440cb55bbe750fe432f2ac81f8437e02c08ee9be5596828d65392eca2664b667e2c608b417daf0db70277fdd51efda33726cbe4 + version: 0.32.0-dev.140 + resolution: "@rocket.chat/icons@npm:0.32.0-dev.140" + checksum: 9da9f6537268a49b814879b2abaaf93de05d42009e023459cf3c2d5966564910ae1ea83a2ccfe6723cfb8077546534d2d281418963d27daf5a58a4ee45029042 languageName: node linkType: hard @@ -5596,6 +5639,13 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/memo@npm:~0.31.19-dev.26": + version: 0.31.19-dev.26 + resolution: "@rocket.chat/memo@npm:0.31.19-dev.26" + checksum: 438059e30795269ab461fd557a9efbad956cbe93624ff5927d3e3abd9132d7cb6b1ac379b4f28efec11c87c34a145a168167e3d96cd111a380aaa23565874c99 + languageName: node + linkType: hard + "@rocket.chat/message-parser@npm:next": version: 0.32.0-dev.99 resolution: "@rocket.chat/message-parser@npm:0.32.0-dev.99" @@ -6140,6 +6190,16 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/styled@npm:~0.31.19-dev.26": + version: 0.31.19-dev.26 + resolution: "@rocket.chat/styled@npm:0.31.19-dev.26" + dependencies: + "@rocket.chat/css-in-js": ~0.31.19-dev.26 + tslib: ^2.3.1 + checksum: 43fe29955169450a0669905ccfcdedecca03cc3e6277be3faeb85b38332afb4df83736da7c5501d452ce869aafd4527af5bacb284948bb5fa83f669f54656142 + languageName: node + linkType: hard + "@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.19": version: 0.31.19-dev.19 resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.19" @@ -6152,6 +6212,18 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.26": + version: 0.31.19-dev.26 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.26" + dependencies: + "@rocket.chat/css-supports": ~0.31.19-dev.26 + tslib: ^2.3.1 + peerDependencies: + stylis: 4.0.10 + checksum: ae845d95833790c60156114878691585de8c24e749f6f52742087584e4ae46b8a364c2ef5a29a7f5cde99de01013961c7475e162daf49b4160b6e81fc1e573b6 + languageName: node + linkType: hard + "@rocket.chat/ui-client@workspace:^, @rocket.chat/ui-client@workspace:packages/ui-client": version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" @@ -29159,7 +29231,7 @@ __metadata: languageName: node linkType: hard -"react-aria@npm:^3.19.0": +"react-aria@npm:^3.19.0, react-aria@npm:~3.19.0": version: 3.19.0 resolution: "react-aria@npm:3.19.0" dependencies: @@ -29412,6 +29484,37 @@ __metadata: languageName: node linkType: hard +"react-stately@npm:~3.17.0": + version: 3.17.0 + resolution: "react-stately@npm:3.17.0" + dependencies: + "@react-stately/calendar": ^3.0.2 + "@react-stately/checkbox": ^3.2.1 + "@react-stately/collections": ^3.4.3 + "@react-stately/combobox": ^3.2.1 + "@react-stately/data": ^3.6.1 + "@react-stately/datepicker": ^3.0.2 + "@react-stately/list": ^3.5.3 + "@react-stately/menu": ^3.4.1 + "@react-stately/numberfield": ^3.2.1 + "@react-stately/overlays": ^3.4.1 + "@react-stately/radio": ^3.5.1 + "@react-stately/searchfield": ^3.3.1 + "@react-stately/select": ^3.3.1 + "@react-stately/selection": ^3.10.3 + "@react-stately/slider": ^3.2.1 + "@react-stately/table": ^3.4.0 + "@react-stately/tabs": ^3.2.1 + "@react-stately/toggle": ^3.4.1 + "@react-stately/tooltip": ^3.2.1 + "@react-stately/tree": ^3.3.3 + "@react-types/shared": ^3.14.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: da233f06f0c4a2b821755cde7b484b8cc07743b075224225a7213be1b62cf1db0966216528775d755fe83cb2a5eeda43c59684bbba47fcf2652f465557f8f4d2 + languageName: node + linkType: hard + "react-transition-group@npm:^4.3.0": version: 4.4.2 resolution: "react-transition-group@npm:4.4.2" -- GitLab From 562f09a528ccaf5cc4201ae0c3b2fff07e627857 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Tue, 4 Oct 2022 13:51:45 -0300 Subject: [PATCH 095/107] Bump version to 5.2.0-rc.2 --- .github/history.json | 174 ++++++++++++++++++++++++++ HISTORY.md | 94 ++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 272 insertions(+), 4 deletions(-) diff --git a/.github/history.json b/.github/history.json index 2efade6727d..ae4aa06918e 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94649,6 +94649,180 @@ ] } ] + }, + "5.1.4": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26965", + "title": "Release 5.1.4 ", + "userLogin": "tassoevan", + "contributors": [ + "aleksandernsilva", + "tassoevan" + ] + } + ] + }, + "5.2.0-rc.2": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26986", + "title": "Regression: Remove section and replace icon on administration kebab menu", + "userLogin": "filipemarins", + "milestone": "5.2.0", + "contributors": [ + "filipemarins", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26975", + "title": "Regression: Omnichannel Contact Center empty on no filter search", + "userLogin": "tiagoevanp", + "milestone": "5.2.0", + "contributors": [ + "tiagoevanp", + "KevLehman", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26987", + "title": "[IMPROVE] Use cached EE Cloud license on startup", + "userLogin": "sampaiodiego", + "milestone": "5.2.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26976", + "title": "Regression: Fixed takeInquiry method not displaying error messages on the client", + "userLogin": "aleksandernsilva", + "description": "This pull request brings back the toast message \"The maximum number of simultaneous chats per agent has been reached.\" that should be displayed when an agent tries to take more chats than the maximum allowed.\r\n\r\n", + "milestone": "5.2.0", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "26988", + "title": "Regression: Handle `undefined` values on `useReactiveQuery`'s query function", + "userLogin": "tassoevan", + "description": "According to https://tanstack.com/query/v4/docs/reference/useQuery, the query function must **not** return `undefined` values, a quirk that we've been ignoring.", + "milestone": "5.2.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "26983", + "title": "Regression: Composer not reactive when omnichannel room closed", + "userLogin": "MartinSchoeler", + "milestone": "5.2.0", + "contributors": [ + "MartinSchoeler", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26687", + "title": "[IMPROVE] Results of user auto complete", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.2.0", + "contributors": [ + "pierre-lehnen-rc", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "26935", + "title": "Regression: Incorrect on-hold chat resume message", + "userLogin": "murtaza98", + "milestone": "5.2.0", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman", + "tassoevan" + ] + }, + { + "pr": "26964", + "title": "Regression: Event handler blocking mention links", + "userLogin": "tassoevan", + "description": "Fixes mention links being irresponsive to clicks.\r\nJira: [TC-55]\r\n\r\n[TC-55]: https://rocketchat.atlassian.net/browse/TC-55?atlOrigin=eyJpIjoiMmQ3ZmE0MWE2NGQwNDIzZThkMzc5NGNhMzU1MjExMGMiLCJwIjoiaiJ9", + "milestone": "5.2.0", + "contributors": [ + "tassoevan", + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "26969", + "title": "Regression: Remove symbols from number before storing PBX event", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26962", + "title": "Regression: Typo on livechat/queue endpoint client call", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26961", + "title": "Revert: \"[IMPROVE] VideoConference Messages UI (#26548)\"", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.2.0", + "contributors": [ + "pierre-lehnen-rc", + "debdutdeb", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "26958", + "title": "[FIX] Admin sidebar navigation", + "userLogin": "juliajforesti", + "description": "## [MKP-125](https://rocketchat.atlassian.net/browse/MKP-125?atlOrigin=eyJpIjoiMThlMTIwYmFkZWI5NDBlYjhlMWI3YTc1ZDliYTY3NDUiLCJwIjoiaiJ9)\r\nFix the behavior of the admin sidebar, it didn't activate its options properly on pages that had sub-routes.\r\nDemo gif:\r\n", + "contributors": [ + "juliajforesti", + "rique223", + "web-flow" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index b878a4ed1a7..980bd156e02 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,79 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.2 +`2022-10-04 · 3 🚀 · 1 🛠· 9 🔠· 13 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +### 🚀 Improvements + + +- Results of user auto complete ([#26687](https://github.com/RocketChat/Rocket.Chat/pull/26687)) + +- Use cached EE Cloud license on startup ([#26987](https://github.com/RocketChat/Rocket.Chat/pull/26987)) + +- VideoConference Messages UI (#26548)" ([#26961](https://github.com/RocketChat/Rocket.Chat/pull/26961)) + +### 🛠Bug fixes + + +- Admin sidebar navigation ([#26958](https://github.com/RocketChat/Rocket.Chat/pull/26958)) + + ## [MKP-125](https://rocketchat.atlassian.net/browse/MKP-125?atlOrigin=eyJpIjoiMThlMTIwYmFkZWI5NDBlYjhlMWI3YTc1ZDliYTY3NDUiLCJwIjoiaiJ9) + Fix the behavior of the admin sidebar, it didn't activate its options properly on pages that had sub-routes. + Demo gif: +  + +<details> +<summary>🔠Minor changes</summary> + + +- Regression: Composer not reactive when omnichannel room closed ([#26983](https://github.com/RocketChat/Rocket.Chat/pull/26983)) + +- Regression: Event handler blocking mention links ([#26964](https://github.com/RocketChat/Rocket.Chat/pull/26964)) + + Fixes mention links being irresponsive to clicks. + Jira: [TC-55] + + [TC-55]: https://rocketchat.atlassian.net/browse/TC-55?atlOrigin=eyJpIjoiMmQ3ZmE0MWE2NGQwNDIzZThkMzc5NGNhMzU1MjExMGMiLCJwIjoiaiJ9 + +- Regression: Fixed takeInquiry method not displaying error messages on the client ([#26976](https://github.com/RocketChat/Rocket.Chat/pull/26976)) + + This pull request brings back the toast message "The maximum number of simultaneous chats per agent has been reached." that should be displayed when an agent tries to take more chats than the maximum allowed. + +  + +- Regression: Handle `undefined` values on `useReactiveQuery`'s query function ([#26988](https://github.com/RocketChat/Rocket.Chat/pull/26988)) + + According to https://tanstack.com/query/v4/docs/reference/useQuery, the query function must **not** return `undefined` values, a quirk that we've been ignoring. + +- Regression: Incorrect on-hold chat resume message ([#26935](https://github.com/RocketChat/Rocket.Chat/pull/26935)) + +- Regression: Omnichannel Contact Center empty on no filter search ([#26975](https://github.com/RocketChat/Rocket.Chat/pull/26975)) + +- Regression: Remove section and replace icon on administration kebab menu ([#26986](https://github.com/RocketChat/Rocket.Chat/pull/26986)) + +- Regression: Remove symbols from number before storing PBX event ([#26969](https://github.com/RocketChat/Rocket.Chat/pull/26969)) + +- Regression: Typo on livechat/queue endpoint client call ([#26962](https://github.com/RocketChat/Rocket.Chat/pull/26962)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@debdutdeb](https://github.com/debdutdeb) +- [@filipemarins](https://github.com/filipemarins) +- [@juliajforesti](https://github.com/juliajforesti) +- [@murtaza98](https://github.com/murtaza98) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@rique223](https://github.com/rique223) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) + ## 5.2.0-rc.1 `2022-09-27 · 1 🛠· 4 🔠· 6 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` @@ -353,6 +426,27 @@ - [@tiagoevanp](https://github.com/tiagoevanp) - [@yash-rajpal](https://github.com/yash-rajpal) +# 5.1.4 +`2022-09-28 · 1 🔠· 2 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +### Engine versions +- Node: `14.19.3` +- NPM: `6.14.17` +- MongoDB: `4.2, 4.4, 5.0` + +<details> +<summary>🔠Minor changes</summary> + + +- Release 5.1.4 ([#26965](https://github.com/RocketChat/Rocket.Chat/pull/26965)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@aleksandernsilva](https://github.com/aleksandernsilva) +- [@tassoevan](https://github.com/tassoevan) + # 5.1.3 `2022-09-24 · 4 🛠· 2 🔠· 8 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 187625b7155..1fcfaa01b6f 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.1 +ENV RC_VERSION 5.2.0-rc.2 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 7894a6b4d28..989ce4a8463 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.1" + "version": "5.2.0-rc.2" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index dc5ea889066..e482c52f46d 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.1", + "version": "5.2.0-rc.2", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 4903263703a..c512370370e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.1", + "version": "5.2.0-rc.2", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From f44b2a34a290c5ae516eb476783950e8f8b6d84e Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Tue, 4 Oct 2022 16:42:31 -0300 Subject: [PATCH 096/107] Chore: `improve/media-recorder` (#26426) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- .../client/imports/components/message-box.css | 9 +- .../client/messageBox/messageBox.html | 2 +- .../client/messageBox/messageBox.ts | 1 - .../client/messageBox/messageBoxActions.ts | 2 +- .../messageBox/messageBoxAudioMessage.html | 22 --- .../messageBox/messageBoxAudioMessage.ts | 173 ---------------- apps/meteor/app/ui/client/index.ts | 2 +- apps/meteor/app/ui/client/lib/fileUpload.ts | 2 +- .../{audioEncoder.js => AudioEncoder.ts} | 21 +- .../ui/client/lib/recorderjs/AudioRecorder.ts | 90 +++++++++ .../ui/client/lib/recorderjs/audioRecorder.js | 105 ---------- apps/meteor/client/templates.ts | 2 + .../AudioMessageRecorder.tsx | 186 ++++++++++++++++++ .../composer/AudioMessageRecorder/index.ts | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 4 +- 15 files changed, 295 insertions(+), 327 deletions(-) delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html delete mode 100644 apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.ts rename apps/meteor/app/ui/client/lib/recorderjs/{audioEncoder.js => AudioEncoder.ts} (72%) create mode 100644 apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts delete mode 100644 apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js create mode 100644 apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx create mode 100644 apps/meteor/client/views/composer/AudioMessageRecorder/index.ts diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index 92c801d2be8..7846eb9366d 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -167,8 +167,7 @@ &-done, &-cancel, - &-timer, - &-loading { + &-timer { display: none; } @@ -205,12 +204,6 @@ } } - &-loading { - cursor: pointer; - - animation: spin 1s linear infinite; - } - &--recording { .rc-message-box__audio-message-mic, .rc-message-box__audio-message-loading { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index 56e1bc9cd0b..898910b3849 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -32,7 +32,7 @@ {{> Template.dynamic template=customAction.template data=customAction.data }} {{ else }} {{#if canSend}} - {{> messageBoxAudioMessage rid=rid tmid=tmid}} + {{> AudioMessageRecorder rid=rid tmid=tmid }} <span class="rc-message-box__action-menu js-action-menu" data-desktop aria-haspopup="true" data-qa-id="menu-more-actions"> {{#if actions}} <span class="rc-message-box__icon"> diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 623eaa2b927..5b2b63574f9 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -25,7 +25,6 @@ import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import './messageBoxActions'; import './messageBoxReplyPreview.ts'; import './userActionIndicator.ts'; -import './messageBoxAudioMessage.ts'; import './messageBox.html'; type MessageBoxTemplateInstance = Blaze.TemplateInstance<{ diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts index e6297e8c734..e99847f8447 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts @@ -53,7 +53,7 @@ messageBox.actions.add('Add_files_from', 'Computer', { }; }); - fileUpload(filesToUpload, $('.js-input-message', messageBox).get(0) as HTMLTextAreaElement | undefined, { rid, tmid }); + fileUpload(filesToUpload, $('.js-input-message', messageBox).get(0) as HTMLTextAreaElement, { rid, tmid }); $input.remove(); }); diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html deleted file mode 100644 index 806ea08ca38..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html +++ /dev/null @@ -1,22 +0,0 @@ -<template name="messageBoxAudioMessage" args="rid"> - {{#if isAllowed}} - <div class="rc-message-box__audio-message {{stateClass}}"> - <div class="rc-message-box__icon rc-message-box__audio-message-cancel js-audio-message-cancel"> - {{> icon block="rc-input__icon-svg" icon="circle-cross"}} - </div> - <div class="rc-message-box__audio-message-timer"> - <span class="rc-message-box__audio-message-timer-dot"></span> - <span class="rc-message-box__audio-message-timer-text">{{time}}</span> - </div> - <div class="rc-message-box__icon rc-message-box__audio-message-done js-audio-message-done"> - {{> icon block="rc-input__icon-svg" icon="checkmark-circled"}} - </div> - <div class="rc-message-box__icon rc-message-box__audio-message-mic js-audio-message-record" data-qa-id="audio-record"> - {{> icon block="rc-input__icon-svg" icon="mic"}} - </div> - <div class="rc-message-box__icon rc-message-box__audio-message-loading js-audio-message-loading"> - {{> icon block="rc-input__icon-svg" icon="loading"}} - </div> - </div> - {{/if}} -</template> diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.ts deleted file mode 100644 index 5630416bc04..00000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import type { Blaze } from 'meteor/blaze'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../settings/client'; -import { AudioRecorder, fileUpload, USER_ACTIVITIES, UserAction } from '../../../ui/client'; -import { t } from '../../../utils/client'; -import './messageBoxAudioMessage.html'; - -type MessageBoxAudioMessageTemplate = Blaze.TemplateInstance<{ - rid: IRoom['_id']; - tmid?: IMessage['_id']; -}> & { - time: ReactiveVar<string>; - state: ReactiveVar<'loading' | 'recording' | 'uploading' | null>; - isMicrophoneDenied: ReactiveVar<boolean>; -}; - -const startRecording = async (rid: IRoom['_id'], tmid?: IMessage['_id']) => { - try { - await AudioRecorder.start(); - UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); - } catch (error) { - throw error; - } -}; - -const stopRecording = async (rid: IRoom['_id'], tmid?: IMessage['_id']): Promise<Blob> => { - const result = await new Promise<Blob>((resolve) => AudioRecorder.stop(resolve)); - UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); - return result; -}; - -const recordingInterval = new ReactiveVar<ReturnType<typeof setInterval> | undefined>(undefined); -const recordingRoomId = new ReactiveVar(null); - -const clearIntervalVariables = () => { - if (recordingInterval.get()) { - clearInterval(recordingInterval.get()); - recordingInterval.set(undefined); - recordingRoomId.set(null); - } -}; - -const cancelRecording = async (instance: MessageBoxAudioMessageTemplate, rid: IRoom['_id'], tmid?: IMessage['_id']): Promise<Blob> => { - clearIntervalVariables(); - - instance.time.set('00:00'); - - const blob = await stopRecording(rid, tmid); - - instance.state.set(null); - - return blob; -}; - -Template.messageBoxAudioMessage.onCreated(async function (this: MessageBoxAudioMessageTemplate) { - this.state = new ReactiveVar(null); - this.time = new ReactiveVar('00:00'); - this.isMicrophoneDenied = new ReactiveVar(false); - - if (navigator.permissions) { - try { - const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); - this.isMicrophoneDenied.set(permissionStatus.state === 'denied'); - permissionStatus.onchange = () => { - this.isMicrophoneDenied.set(permissionStatus.state === 'denied'); - }; - return; - } catch (error) { - console.warn(error); - } - } - - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - this.isMicrophoneDenied.set(true); - return; - } - - try { - if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { - this.isMicrophoneDenied.set(true); - return; - } - } catch (error) { - console.warn(error); - } -}); - -Template.messageBoxAudioMessage.onDestroyed(async function (this: MessageBoxAudioMessageTemplate) { - if (this.state.get() === 'recording') { - const { rid, tmid } = this.data; - await cancelRecording(this, rid, tmid); - } -}); - -Template.messageBoxAudioMessage.helpers({ - isAllowed() { - return ( - AudioRecorder.isSupported() && - !(Template.instance() as MessageBoxAudioMessageTemplate).isMicrophoneDenied.get() && - settings.get('FileUpload_Enabled') && - settings.get('Message_AudioRecorderEnabled') && - (!settings.get('FileUpload_MediaTypeBlackList') || !settings.get('FileUpload_MediaTypeBlackList').match(/audio\/mp3|audio\/\*/i)) && - (!settings.get('FileUpload_MediaTypeWhiteList') || settings.get('FileUpload_MediaTypeWhiteList').match(/audio\/mp3|audio\/\*/i)) - ); - }, - - stateClass() { - if (recordingRoomId.get() && recordingRoomId.get() !== Template.currentData().rid) { - return 'rc-message-box__audio-message--busy'; - } - - const state = (Template.instance() as MessageBoxAudioMessageTemplate).state.get(); - return state && `rc-message-box__audio-message--${state}`; - }, - - time() { - return (Template.instance() as MessageBoxAudioMessageTemplate).time.get(); - }, -}); - -Template.messageBoxAudioMessage.events({ - async 'click .js-audio-message-record'(event: JQuery.ClickEvent, instance: MessageBoxAudioMessageTemplate) { - event.preventDefault(); - - if (recordingRoomId.get() && recordingRoomId.get() !== this.rid) { - return; - } - - instance.state.set('recording'); - - try { - await startRecording(this.rid, this.tmid); - const startTime = new Date(); - recordingInterval.set( - setInterval(() => { - const now = new Date(); - const distance = (now.getTime() - startTime.getTime()) / 1000; - const minutes = Math.floor(distance / 60); - const seconds = Math.floor(distance % 60); - instance.time.set(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); - }, 1000), - ); - recordingRoomId.set(this.rid); - } catch (error) { - console.log(error); - instance.isMicrophoneDenied.set(true); - instance.state.set(null); - } - }, - - async 'click .js-audio-message-cancel'(event: JQuery.ClickEvent, instance: MessageBoxAudioMessageTemplate) { - event.preventDefault(); - - await cancelRecording(instance, this.rid, this.tmid); - }, - - async 'click .js-audio-message-done'(event: JQuery.ClickEvent, instance: MessageBoxAudioMessageTemplate) { - event.preventDefault(); - - instance.state.set('loading'); - - const { rid, tmid } = this; - const blob = await cancelRecording(instance, rid, tmid); - - const fileName = `${t('Audio record')}.mp3`; - const file = new File([blob], fileName, { type: 'audio/mpeg' }); - - await fileUpload([{ file, name: fileName }], undefined, { rid, tmid }); - }, -}); diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index f7f67f76a70..8cff04d064b 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -22,6 +22,6 @@ export { fileUpload } from './lib/fileUpload'; export { UserAction, USER_ACTIVITIES } from './lib/UserAction'; export { KonchatNotification } from './lib/notification'; export { Login, Button } from './lib/rocket'; -export { AudioRecorder } from './lib/recorderjs/audioRecorder'; +export { AudioRecorder } from './lib/recorderjs/AudioRecorder'; export { VideoRecorder } from './lib/recorderjs/videoRecorder'; export * from './lib/userPopoverStatus'; diff --git a/apps/meteor/app/ui/client/lib/fileUpload.ts b/apps/meteor/app/ui/client/lib/fileUpload.ts index d5c5a22fbe5..84935425da8 100644 --- a/apps/meteor/app/ui/client/lib/fileUpload.ts +++ b/apps/meteor/app/ui/client/lib/fileUpload.ts @@ -158,7 +158,7 @@ export type FileUploadProp = SingleOrArray<{ /* @deprecated */ export const fileUpload = async ( f: FileUploadProp, - input: HTMLTextAreaElement | undefined, + input: HTMLInputElement | ArrayLike<HTMLInputElement> | HTMLTextAreaElement, { rid, tmid, diff --git a/apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts similarity index 72% rename from apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js rename to apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts index 81934c69c19..7f7dcb169db 100644 --- a/apps/meteor/app/ui/client/lib/recorderjs/audioEncoder.js +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts @@ -1,10 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Emitter } from '@rocket.chat/emitter'; -import { settings } from '../../../../settings'; +export class AudioEncoder extends Emitter { + private worker: Worker; -class AudioEncoder extends Emitter { - constructor(source, { bufferLen = 4096, numChannels = 1, bitRate = settings.get('Message_Audio_bitRate') || 32 } = {}) { + private scriptNode: ScriptProcessorNode; + + constructor(source: MediaStreamAudioSourceNode, { bufferLen = 4096, numChannels = 1, bitRate = 32 } = {}) { super(); const workerPath = Meteor.absoluteUrl('workers/mp3-encoder/index.js'); @@ -21,12 +23,7 @@ class AudioEncoder extends Emitter { }, }); - this.scriptNode = (source.context.createScriptProcessor || source.context.createJavaScriptNode).call( - source.context, - bufferLen, - numChannels, - numChannels, - ); + this.scriptNode = source.context.createScriptProcessor(bufferLen, numChannels, numChannels); this.scriptNode.onaudioprocess = this.handleAudioProcess; source.connect(this.scriptNode); @@ -37,7 +34,7 @@ class AudioEncoder extends Emitter { this.worker.postMessage({ command: 'finish' }); } - handleWorkerMessage = (event) => { + handleWorkerMessage = (event: MessageEvent) => { switch (event.data.command) { case 'end': { // prepend mp3 magic number to the buffer @@ -51,7 +48,7 @@ class AudioEncoder extends Emitter { } }; - handleAudioProcess = (event) => { + handleAudioProcess = (event: AudioProcessingEvent) => { for (let channel = 0; channel < event.inputBuffer.numberOfChannels; channel++) { const buffer = event.inputBuffer.getChannelData(channel); this.worker.postMessage({ @@ -61,5 +58,3 @@ class AudioEncoder extends Emitter { } }; } - -export { AudioEncoder }; diff --git a/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts new file mode 100644 index 00000000000..c4b55035965 --- /dev/null +++ b/apps/meteor/app/ui/client/lib/recorderjs/AudioRecorder.ts @@ -0,0 +1,90 @@ +import { AudioEncoder } from './AudioEncoder'; +import { settings } from '../../../../settings/client'; + +export class AudioRecorder { + private audioContext: AudioContext | undefined; + + private stream: MediaStream | undefined; + + private encoder: AudioEncoder | undefined; + + isSupported() { + return Boolean(navigator.mediaDevices?.getUserMedia) && Boolean(AudioContext); + } + + createAudioContext() { + if (this.audioContext) { + return; + } + + this.audioContext = new AudioContext(); + } + + destroyAudioContext() { + if (!this.audioContext) { + return; + } + + this.audioContext.close(); + delete this.audioContext; + } + + async createStream() { + if (this.stream) { + return; + } + + this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } + + destroyStream() { + if (!this.stream) { + return; + } + + this.stream.getAudioTracks().forEach((track) => track.stop()); + delete this.stream; + } + + async createEncoder() { + if (this.encoder || !this.audioContext || !this.stream) { + return; + } + + const input = this.audioContext?.createMediaStreamSource(this.stream); + this.encoder = new AudioEncoder(input, { bitRate: settings.get('Message_Audio_bitRate') || 32 }); + } + + destroyEncoder() { + if (!this.encoder) { + return; + } + + this.encoder.close(); + delete this.encoder; + } + + async start(cb?: (this: this, done: boolean) => void) { + try { + this.createAudioContext(); + await this.createStream(); + await this.createEncoder(); + cb?.call(this, true); + } catch (error) { + console.error(error); + this.destroyEncoder(); + this.destroyStream(); + this.destroyAudioContext(); + cb?.call(this, false); + } + } + + stop(cb: (data: Blob) => void) { + this.encoder?.on('encoded', cb); + this.encoder?.close(); + + this.destroyEncoder(); + this.destroyStream(); + this.destroyAudioContext(); + } +} diff --git a/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js b/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js deleted file mode 100644 index 51f83a2091e..00000000000 --- a/apps/meteor/app/ui/client/lib/recorderjs/audioRecorder.js +++ /dev/null @@ -1,105 +0,0 @@ -import { AudioEncoder } from './audioEncoder'; - -const getUserMedia = ((navigator) => { - if (navigator.mediaDevices) { - return navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - } - - const legacyGetUserMedia = - navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; - - if (legacyGetUserMedia) { - return (options) => - new Promise((resolve, reject) => { - legacyGetUserMedia.call(navigator, options, resolve, reject); - }); - } -})(window.navigator); - -const AudioContext = window.AudioContext || window.webkitAudioContext; - -class AudioRecorder { - isSupported() { - return Boolean(getUserMedia) && Boolean(AudioContext); - } - - createAudioContext() { - if (this.audioContext) { - return; - } - - this.audioContext = new AudioContext(); - } - - destroyAudioContext() { - if (!this.audioContext) { - return; - } - - this.audioContext.close(); - delete this.audioContext; - } - - async createStream() { - if (this.stream) { - return; - } - - this.stream = await getUserMedia({ audio: true }); - } - - destroyStream() { - if (!this.stream) { - return; - } - - this.stream.getAudioTracks().forEach((track) => track.stop()); - delete this.stream; - } - - async createEncoder() { - if (this.encoder) { - return; - } - - const input = this.audioContext.createMediaStreamSource(this.stream); - this.encoder = new AudioEncoder(input); - } - - destroyEncoder() { - if (!this.encoder) { - return; - } - - this.encoder.close(); - delete this.encoder; - } - - async start(cb) { - try { - await this.createAudioContext(); - await this.createStream(); - await this.createEncoder(); - cb && cb.call(this, true); - } catch (error) { - console.error(error); - this.destroyEncoder(); - this.destroyStream(); - this.destroyAudioContext(); - cb && cb.call(this, false); - } - } - - stop(cb) { - this.encoder.on('encoded', cb); - this.encoder.close(); - - this.destroyEncoder(); - this.destroyStream(); - this.destroyAudioContext(); - } -} - -const instance = new AudioRecorder(); - -export { instance as AudioRecorder }; diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index c645b7ebf2a..c0368a7c206 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -79,3 +79,5 @@ createTemplateForComponent('sidebarFooter', () => import('./sidebar/footer')); createTemplateForComponent('loggedOutBanner', () => import('../ee/client/components/deviceManagement/LoggedOutBanner'), { renderContainerView: () => HTML.DIV({ style: 'max-width: 520px; margin: 0 auto;' }), }); + +createTemplateForComponent('AudioMessageRecorder', () => import('./views/composer/AudioMessageRecorder')); diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx new file mode 100644 index 00000000000..5d9e8763bc7 --- /dev/null +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/AudioMessageRecorder.tsx @@ -0,0 +1,186 @@ +import { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { Icon, Throbber } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useEffect, useMemo, useState } from 'react'; + +import { AudioRecorder, fileUpload, UserAction, USER_ACTIVITIES } from '../../../../app/ui/client'; + +const audioRecorder = new AudioRecorder(); + +type AudioMessageRecorderProps = { + rid: IRoom['_id']; + tmid: IMessage['_id']; +}; + +const AudioMessageRecorder = ({ rid, tmid }: AudioMessageRecorderProps): ReactElement | null => { + const t = useTranslation(); + + const [state, setState] = useState<'idle' | 'loading' | 'recording'>('idle'); + const [time, setTime] = useState('00:00'); + const [isMicrophoneDenied, setIsMicrophoneDenied] = useState(false); + const [recordingInterval, setRecordingInterval] = useState<ReturnType<typeof setInterval> | null>(null); + const [recordingRoomId, setRecordingRoomId] = useState<IRoom['_id'] | null>(null); + + const stopRecording = useMutableCallback(async () => { + if (recordingInterval) { + clearInterval(recordingInterval); + } + setRecordingInterval(null); + setRecordingRoomId(null); + + setTime('00:00'); + + const blob = await new Promise<Blob>((resolve) => audioRecorder.stop(resolve)); + UserAction.stop(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); + + setState('idle'); + + return blob; + }); + + const handleMount = useMutableCallback(async (): Promise<void> => { + if (navigator.permissions) { + try { + const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }); + setIsMicrophoneDenied(permissionStatus.state === 'denied'); + permissionStatus.onchange = (): void => { + setIsMicrophoneDenied(permissionStatus.state === 'denied'); + }; + return; + } catch (error) { + console.warn(error); + } + } + + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + setIsMicrophoneDenied(true); + return; + } + + try { + if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) { + setIsMicrophoneDenied(true); + return; + } + } catch (error) { + console.warn(error); + } + }); + + const handleUnmount = useMutableCallback(async () => { + if (state === 'recording') { + await stopRecording(); + } + }); + + useEffect(() => { + handleMount(); + + return () => { + handleUnmount(); + }; + }, [handleMount, handleUnmount]); + + const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; + const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; + const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; + const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; + + const isAllowed = useMemo( + () => + audioRecorder.isSupported() && + !isMicrophoneDenied && + isFileUploadEnabled && + isAudioRecorderEnabled && + (!fileUploadMediaTypeBlackList || !fileUploadMediaTypeBlackList.match(/audio\/mp3|audio\/\*/i)) && + (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)), + [fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied], + ); + + const stateClass = useMemo(() => { + if (recordingRoomId && recordingRoomId !== rid) { + return 'rc-message-box__audio-message--busy'; + } + + return state && `rc-message-box__audio-message--${state}`; + }, [recordingRoomId, rid, state]); + + const handleRecordButtonClick = useMutableCallback(async () => { + if (recordingRoomId && recordingRoomId !== rid) { + return; + } + + setState('recording'); + + try { + await audioRecorder.start(); + UserAction.performContinuously(rid, USER_ACTIVITIES.USER_RECORDING, { tmid }); + const startTime = new Date(); + setRecordingInterval( + setInterval(() => { + const now = new Date(); + const distance = (now.getTime() - startTime.getTime()) / 1000; + const minutes = Math.floor(distance / 60); + const seconds = Math.floor(distance % 60); + setTime(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); + }, 1000), + ); + setRecordingRoomId(rid); + } catch (error) { + console.log(error); + setIsMicrophoneDenied(true); + setState('idle'); + } + }); + + const handleCancelButtonClick = useMutableCallback(async () => { + await stopRecording(); + }); + + const handleDoneButtonClick = useMutableCallback(async () => { + setState('loading'); + + const blob = await stopRecording(); + + const fileName = `${t('Audio_record')}.mp3`; + const file = new File([blob], fileName, { type: 'audio/mpeg' }); + + await fileUpload([{ file, name: fileName }], [], { rid, tmid }); + }); + + if (!isAllowed) { + return null; + } + + return ( + <div className={`rc-message-box__audio-message ${stateClass}`}> + {state === 'recording' && ( + <> + <div className='rc-message-box__icon rc-message-box__audio-message-cancel' onClick={handleCancelButtonClick}> + <Icon name='circle-cross' size={24} /> + </div> + <div className='rc-message-box__audio-message-timer'> + <span className='rc-message-box__audio-message-timer-dot'></span> + <span className='rc-message-box__audio-message-timer-text'>{time}</span> + </div> + <div className='rc-message-box__icon rc-message-box__audio-message-done' onClick={handleDoneButtonClick}> + <Icon name='circle-check' size={24} /> + </div> + </> + )} + {state === 'idle' && ( + <div className='rc-message-box__icon rc-message-box__audio-message-mic' data-qa-id='audio-record' onClick={handleRecordButtonClick}> + <Icon name='mic' size={24} /> + </div> + )} + {state === 'loading' && ( + <div className='rc-message-box__icon'> + <Throbber inheritColor size='x12' /> + </div> + )} + </div> + ); +}; + +export default AudioMessageRecorder; diff --git a/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts b/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts new file mode 100644 index 00000000000..a18f21430c5 --- /dev/null +++ b/apps/meteor/client/views/composer/AudioMessageRecorder/index.ts @@ -0,0 +1 @@ +export { default } from './AudioMessageRecorder'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index bab13b264e5..d6fba1afc91 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -615,6 +615,7 @@ "Audio_Notification_Value_Description": "Can be any custom sound or the default ones: beep, chelle, ding, droplet, highbell, seasons", "Audio_Notifications_Default_Alert": "Audio Notifications Default Alert", "Audio_Notifications_Value": "Default Message Notification Audio", + "Audio_record": "Audio record", "Audios": "Audios", "Audit": "Audit", "Auditing": "Auditing", @@ -5459,5 +5460,6 @@ "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", "onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store", "onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services", + "Something_Went_Wrong": "Something went wrong", "Toolbox_room_actions": "Primary Room actions" -} \ No newline at end of file +} -- GitLab From 3715e0d1096dd92d72ec585dd1800c431f8633b4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Tue, 4 Oct 2022 15:15:10 -0600 Subject: [PATCH 097/107] Regression: Use raw models instead of meteor ones on visitor inactivity processing (#27002) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../server/lib/VisitorInactivityMonitor.js | 18 +++++++++--------- .../app/livechat-enterprise/server/startup.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index b794faf810b..fe4df546eb3 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -1,10 +1,9 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Meteor } from 'meteor/meteor'; -import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; +import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; -import { LivechatDepartment, Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; import { LivechatEnterprise } from './LivechatEnterprise'; @@ -15,10 +14,10 @@ export class VisitorInactivityMonitor { this.messageCache = new Map(); } - start() { + async start() { this._startMonitoring(); this._initializeMessageCache(); - this.user = Users.findOneById('rocket.cat'); + this.user = await Users.findOneById('rocket.cat'); } _startMonitoring() { @@ -55,11 +54,11 @@ export class VisitorInactivityMonitor { this.messageCache.set('default', settings.get('Livechat_abandoned_rooms_closed_custom_message') || TAPi18n.__('Closed_automatically')); } - _getDepartmentAbandonedCustomMessage(departmentId) { + async _getDepartmentAbandonedCustomMessage(departmentId) { if (this.messageCache.has('departmentId')) { return this.messageCache.get('departmentId'); } - const department = LivechatDepartment.findOneById(departmentId); + const department = await LivechatDepartment.findOneById(departmentId); if (!department) { return; } @@ -67,10 +66,10 @@ export class VisitorInactivityMonitor { return department.abandonedRoomsCloseCustomMessage; } - closeRooms(room) { + async closeRooms(room) { let comment = this.messageCache.get('default'); if (room.departmentId) { - comment = this._getDepartmentAbandonedCustomMessage(room.departmentId) || comment; + comment = (await this._getDepartmentAbandonedCustomMessage(room.departmentId)) || comment; } Livechat.closeRoom({ comment, @@ -100,10 +99,11 @@ export class VisitorInactivityMonitor { if (!action || action === 'none') { return; } + // TODO: change this to an async map or reduce Promise.await(LivechatRooms.findAbandonedOpenRooms(new Date())).forEach((room) => { switch (action) { case 'close': { - this.closeRooms(room); + Promise.await(this.closeRooms(room)); break; } case 'on-hold': { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/startup.ts b/apps/meteor/ee/app/livechat-enterprise/server/startup.ts index 97b025e5928..22bdbe8b0a6 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/startup.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/startup.ts @@ -25,7 +25,7 @@ Meteor.startup(async function () { if (!value || value === 'none') { return visitorActivityMonitor.stop(); } - visitorActivityMonitor.start(); + Promise.await(visitorActivityMonitor.start()); }); settings.change('Livechat_visitor_inactivity_timeout', function () { Promise.await(updatePredictedVisitorAbandonment()); -- GitLab From 1f9ab1aa6fb573ed7c0275de2619ec0e4331bc50 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Tue, 4 Oct 2022 18:19:26 -0300 Subject: [PATCH 098/107] Bump version to 5.2.0-rc.3 --- .github/history.json | 31 +++++++++++++++++++++++++++ HISTORY.md | 19 ++++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/.github/history.json b/.github/history.json index ae4aa06918e..63fba22c6a3 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94823,6 +94823,37 @@ ] } ] + }, + "5.2.0-rc.3": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "27002", + "title": "Regression: Use raw models instead of meteor ones on visitor inactivity processing", + "userLogin": "KevLehman", + "milestone": "5.2.0", + "contributors": [ + "KevLehman", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26426", + "title": "Chore: `improve/media-recorder`", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "ggazzo" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 980bd156e02..87b78ed80d8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,25 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.3 +`2022-10-04 · 2 🔠· 3 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +<details> +<summary>🔠Minor changes</summary> + + +- Chore: `improve/media-recorder` ([#26426](https://github.com/RocketChat/Rocket.Chat/pull/26426)) + +- Regression: Use raw models instead of meteor ones on visitor inactivity processing ([#27002](https://github.com/RocketChat/Rocket.Chat/pull/27002)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@ggazzo](https://github.com/ggazzo) +- [@tassoevan](https://github.com/tassoevan) + ## 5.2.0-rc.2 `2022-10-04 · 3 🚀 · 1 🛠· 9 🔠· 13 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 1fcfaa01b6f..b2196db29bd 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.2 +ENV RC_VERSION 5.2.0-rc.3 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 989ce4a8463..1a36be98213 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.2" + "version": "5.2.0-rc.3" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index e482c52f46d..ee5262ff0d5 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.2", + "version": "5.2.0-rc.3", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index c512370370e..03015b450f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.2", + "version": "5.2.0-rc.3", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From 4afcbf64a26524b3fad9d213d276800bd35323a2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo <guilhermegazzo@gmail.com> Date: Wed, 5 Oct 2022 14:02:21 -0300 Subject: [PATCH 099/107] Chore: break LDAP manager into smaller pieces to improve unit tests (#26994) * break LDAP manager into smaller pieces to improve unit tests * Fix * Apply suggestions from code review Co-authored-by: Diego Sampaio <chinello@gmail.com> * Apply suggestions from code review Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> * Added few tests Co-authored-by: Diego Sampaio <chinello@gmail.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- apps/meteor/.mocharc.js | 1 + .../server/classes/ImportDataConverter.ts | 13 +- apps/meteor/app/utils/client/index.ts | 1 - .../app/utils/lib/templateVarHandler.js | 9 +- apps/meteor/ee/server/lib/ldap/Manager.ts | 91 +------- .../lib/ldap/copyCustomFieldsLDAP.spec.ts | 209 ++++++++++++++++++ .../server/lib/ldap/copyCustomFieldsLDAP.ts | 71 ++++++ .../ee/server/lib/ldap/getNestedProp.spec.ts | 23 ++ .../ee/server/lib/ldap/getNestedProp.ts | 7 + .../lib/ldap/replacesNestedValues.spec.ts | 87 ++++++++ .../server/lib/ldap/replacesNestedValues.ts | 26 +++ 11 files changed, 445 insertions(+), 93 deletions(-) create mode 100644 apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.spec.ts create mode 100644 apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts create mode 100644 apps/meteor/ee/server/lib/ldap/getNestedProp.spec.ts create mode 100644 apps/meteor/ee/server/lib/ldap/getNestedProp.ts create mode 100644 apps/meteor/ee/server/lib/ldap/replacesNestedValues.spec.ts create mode 100644 apps/meteor/ee/server/lib/ldap/replacesNestedValues.ts diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index f8118d74f7c..3a7c511d8a2 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -24,6 +24,7 @@ module.exports = { ...base, // see https://github.com/mochajs/mocha/issues/3916 exit: true, spec: [ + 'ee/server/lib/ldap/*.spec.ts', 'ee/tests/**/*.tests.ts', 'ee/tests/**/*.spec.ts', 'tests/unit/app/**/*.spec.ts', diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 74eec85b942..3d10e43ff09 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -217,7 +217,10 @@ export class ImportDataConverter { continue; } - updateData.$set[keyPath] = source[key]; + updateData.$set = { + ...updateData.$set, + ...{ [keyPath]: source[key] }, + }; } }; @@ -237,16 +240,16 @@ export class ImportDataConverter { } // #ToDo: #TODO: Move this to the model class - const updateData: Record<string, any> = { - $set: { + const updateData: Record<string, any> = Object.assign(Object.create(null), { + $set: Object.assign(Object.create(null), { ...(userData.roles && { roles: userData.roles }), ...(userData.type && { type: userData.type }), ...(userData.statusText && { statusText: userData.statusText }), ...(userData.bio && { bio: userData.bio }), ...(userData.services?.ldap && { ldap: true }), ...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }), - }, - }; + }), + }); this.addCustomFields(updateData, userData); this.addUserServices(updateData, userData); diff --git a/apps/meteor/app/utils/client/index.ts b/apps/meteor/app/utils/client/index.ts index 26bc7cd5d5b..8029a0d4826 100644 --- a/apps/meteor/app/utils/client/index.ts +++ b/apps/meteor/app/utils/client/index.ts @@ -9,6 +9,5 @@ export { getUserNotificationPreference } from '../lib/getUserNotificationPrefere export { getAvatarColor } from '../lib/getAvatarColor'; export { getURL } from '../lib/getURL'; export { placeholders } from '../lib/placeholders'; -export { templateVarHandler } from '../lib/templateVarHandler'; export { APIClient } from './lib/RestApiClient'; export { secondsToHHMMSS } from '../../../lib/utils/secondsToHHMMSS'; diff --git a/apps/meteor/app/utils/lib/templateVarHandler.js b/apps/meteor/app/utils/lib/templateVarHandler.js index a04c8391e15..309cdc49bb2 100644 --- a/apps/meteor/app/utils/lib/templateVarHandler.js +++ b/apps/meteor/app/utils/lib/templateVarHandler.js @@ -1,11 +1,6 @@ -import { Meteor } from 'meteor/meteor'; +import { Logger } from '../../../server/lib/logger/Logger'; -let logger; - -if (Meteor.isServer) { - const { Logger } = require('../../../server/lib/logger/Logger'); - logger = new Logger('TemplateVarHandler'); -} +const logger = new Logger('TemplateVarHandler'); export const templateVarHandler = function (variable, object) { const templateRegex = /#{([\w\-]+)}/gi; diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 1be6c3bb78d..02c7766e592 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -1,4 +1,3 @@ -import _ from 'underscore'; import type ldapjs from 'ldapjs'; import type { ILDAPEntry, IUser, IRoom, ICreatedRoom, IRole, IImportUser } from '@rocket.chat/core-typings'; import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models'; @@ -10,11 +9,11 @@ import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; import { LDAPManager } from '../../../../server/lib/ldap/Manager'; import { logger, searchLogger, mapLogger } from '../../../../server/lib/ldap/Logger'; -import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions'; import { syncUserRoles } from '../syncUserRoles'; import { Team } from '../../../../server/sdk'; import { ensureArray } from '../../../../lib/utils/arrayUtils'; +import { copyCustomFieldsLDAP } from './copyCustomFieldsLDAP'; export class LDAPEEManager extends LDAPManager { public static async sync(): Promise<void> { @@ -506,76 +505,16 @@ export class LDAPEEManager extends LDAPManager { } public static copyCustomFields(ldapUser: ILDAPEntry, userData: IImportUser): void { - if (!settings.get<boolean>('LDAP_Sync_Custom_Fields')) { - return; - } - - const customFieldsSettings = settings.get<string>('Accounts_CustomFields'); - const customFieldsMap = settings.get<string>('LDAP_CustomFieldMap'); - - if (!customFieldsMap || !customFieldsSettings) { - if (customFieldsMap) { - logger.debug('Skipping LDAP custom fields because there are no custom fields configured.'); - } - return; - } - - let map: Record<string, string>; - try { - map = JSON.parse(customFieldsMap) as Record<string, string>; - } catch (error) { - logger.error('Failed to parse LDAP Custom Fields mapping'); - logger.error(error); - return; - } - - let customFields: Record<string, any>; - try { - customFields = JSON.parse(customFieldsSettings) as Record<string, any>; - } catch (error) { - logger.error('Failed to parse Custom Fields'); - logger.error(error); - return; - } - - _.map(map, (userField, ldapField) => { - if (!this.getCustomField(customFields, userField)) { - logger.debug(`User attribute does not exist: ${userField}`); - return; - } - - if (!userData.customFields) { - userData.customFields = {}; - } - - const value = templateVarHandler(ldapField, ldapUser); - - if (value) { - let ref: Record<string, any> = userData.customFields; - const attributeNames = userField.split('.'); - let previousKey: string | undefined; - - for (const key of attributeNames) { - if (previousKey) { - if (ref[previousKey] === undefined) { - ref[previousKey] = {}; - } else if (typeof ref[previousKey] !== 'object') { - logger.error(`Failed to assign custom field: ${userField}`); - return; - } - - ref = ref[previousKey]; - } - - previousKey = key; - } - - if (previousKey) { - ref[previousKey] = value; - logger.debug(`user.customFields.${userField} changed to: ${value}`); - } - } - }); + return copyCustomFieldsLDAP( + { + ldapUser, + userData, + customFieldsSettings: settings.get<string>('Accounts_CustomFields'), + customFieldsMap: settings.get<string>('LDAP_CustomFieldMap'), + syncCustomFields: settings.get<boolean>('LDAP_Sync_Custom_Fields'), + }, + logger, + ); } private static async importNewUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> { @@ -660,12 +599,4 @@ export class LDAPEEManager extends LDAPManager { } } } - - private static getCustomField(customFields: Record<string, any>, property: string): any { - try { - return _.reduce(property.split('.'), (acc, el) => acc[el], customFields); - } catch { - // ignore errors - } - } } diff --git a/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.spec.ts b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.spec.ts new file mode 100644 index 00000000000..fade0e19e89 --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.spec.ts @@ -0,0 +1,209 @@ +import type { IImportUser, ILDAPEntry } from '@rocket.chat/core-typings'; +import { expect, spy } from 'chai'; + +import { copyCustomFieldsLDAP } from './copyCustomFieldsLDAP'; +import type { Logger } from '../../../../app/logger/server'; + +describe('LDAP copyCustomFieldsLDAP', () => { + it('should copy custom fields from ldapUser to rcUser', () => { + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: true, + customFieldsSettings: JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + }), + customFieldsMap: JSON.stringify({ + givenName: 'mappedGivenName', + }), + }, + { + debug: () => undefined, + error: () => undefined, + } as unknown as Logger, + ); + + expect(userData).to.have.property('customFields'); + expect(userData.customFields).to.be.eql({ mappedGivenName: 'Test' }); + }); + + it('should copy custom fields from ldapUser to rcUser already having other custom fields', () => { + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + customFields: { + custom: 'Test', + }, + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: true, + customFieldsSettings: JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + }), + customFieldsMap: JSON.stringify({ + givenName: 'mappedGivenName', + }), + }, + { + debug: () => undefined, + error: () => undefined, + } as unknown as Logger, + ); + + expect(userData).to.have.property('customFields'); + expect(userData.customFields).to.be.eql({ custom: 'Test', mappedGivenName: 'Test' }); + }); + + it('should not copy custom fields from ldapUser to rcUser if syncCustomFields is false', () => { + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: false, + customFieldsSettings: JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + }), + customFieldsMap: JSON.stringify({ + givenName: 'mappedGivenName', + }), + }, + { + debug: () => undefined, + error: () => undefined, + } as unknown as Logger, + ); + + expect(userData).to.not.have.property('customFields'); + }); + + it('should call logger.error if customFieldsSettings is not a valid JSON', () => { + const debug = spy(); + const error = spy(); + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: true, + customFieldsSettings: `${JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + })}}`, + customFieldsMap: JSON.stringify({ + givenName: 'mappedGivenName', + }), + }, + { + debug, + error, + } as unknown as Logger, + ); + expect(error).to.have.been.called.exactly(1); + }); + it('should call logger.error if customFieldsMap is not a valid JSON', () => { + const debug = spy(); + const error = spy(); + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: true, + customFieldsSettings: JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + }), + customFieldsMap: `${JSON.stringify({ + givenName: 'mappedGivenName', + })}}`, + }, + { + debug, + error, + } as unknown as Logger, + ); + expect(error).to.have.been.called.exactly(1); + }); + + it('should call logger.debug if some custom fields are not mapped but still mapping the other fields', () => { + const debug = spy(); + const error = spy(); + const ldapUser = { + mail: 'test@test.com', + givenName: 'Test', + } as unknown as ILDAPEntry; + + const userData = { + name: 'Test', + username: 'test', + } as unknown as IImportUser; + + copyCustomFieldsLDAP( + { + ldapUser, + userData, + syncCustomFields: true, + customFieldsSettings: JSON.stringify({ + mappedGivenName: { type: 'text', required: false }, + }), + customFieldsMap: JSON.stringify({ + givenName: 'mappedGivenName', + test: 'test', + }), + }, + { + debug, + error, + } as unknown as Logger, + ); + expect(debug).to.have.been.called.exactly(1); + expect(userData).to.have.property('customFields'); + expect(userData.customFields).to.be.eql({ mappedGivenName: 'Test' }); + }); +}); diff --git a/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts new file mode 100644 index 00000000000..d3ab09d7b24 --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/copyCustomFieldsLDAP.ts @@ -0,0 +1,71 @@ +import type { IImportUser, ILDAPEntry } from '@rocket.chat/core-typings'; + +import type { Logger } from '../../../../app/logger/server'; +import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; +import { getNestedProp } from './getNestedProp'; +import { replacesNestedValues } from './replacesNestedValues'; + +export const copyCustomFieldsLDAP = ( + { + ldapUser, + userData, + customFieldsSettings, + customFieldsMap, + syncCustomFields, + }: { + ldapUser: ILDAPEntry; + userData: IImportUser; + syncCustomFields: boolean; + customFieldsSettings: string; + customFieldsMap: string; + }, + logger: Logger, +): void => { + if (!syncCustomFields) { + return; + } + + if (!customFieldsMap || !customFieldsSettings) { + if (customFieldsMap) { + logger.debug('Skipping LDAP custom fields because there are no custom map fields configured.'); + return; + } + logger.debug('Skipping LDAP custom fields because there are no custom fields configured.'); + return; + } + + const map: Record<string, string> = (() => { + try { + return JSON.parse(customFieldsMap); + } catch (err) { + logger.error({ msg: 'Error parsing LDAP custom fields map.', err }); + } + })(); + + if (!map) { + return; + } + + let customFields: Record<string, any>; + try { + customFields = JSON.parse(customFieldsSettings) as Record<string, any>; + } catch (err) { + logger.error({ msg: 'Failed to parse Custom Fields', err }); + return; + } + + Object.entries(map).forEach(([ldapField, userField]) => { + if (!getNestedProp(customFields, userField)) { + logger.debug(`User attribute does not exist: ${userField}`); + return; + } + + const value = templateVarHandler(ldapField, ldapUser); + + if (!value) { + return; + } + + userData.customFields = replacesNestedValues({ ...userData.customFields }, userField, value); + }); +}; diff --git a/apps/meteor/ee/server/lib/ldap/getNestedProp.spec.ts b/apps/meteor/ee/server/lib/ldap/getNestedProp.spec.ts new file mode 100644 index 00000000000..b751f444193 --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/getNestedProp.spec.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai'; + +import { getNestedProp } from './getNestedProp'; + +describe('LDAP getNestedProp', () => { + it('should find shallow values', () => { + const customFields = { + foo: 'bar', + }; + + expect(getNestedProp(customFields, 'foo')).to.equal('bar'); + }); + + it('should find deep values', () => { + const customFields = { + foo: { + bar: 'baz', + }, + }; + + expect(getNestedProp(customFields, 'foo.bar')).to.equal('baz'); + }); +}); diff --git a/apps/meteor/ee/server/lib/ldap/getNestedProp.ts b/apps/meteor/ee/server/lib/ldap/getNestedProp.ts new file mode 100644 index 00000000000..9836683801d --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/getNestedProp.ts @@ -0,0 +1,7 @@ +export const getNestedProp = (customFields: Record<string, any>, property: string): unknown => { + try { + return property.split('.').reduce((acc, el) => acc[el], customFields); + } catch { + // ignore errors + } +}; diff --git a/apps/meteor/ee/server/lib/ldap/replacesNestedValues.spec.ts b/apps/meteor/ee/server/lib/ldap/replacesNestedValues.spec.ts new file mode 100644 index 00000000000..a6ead7a087d --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/replacesNestedValues.spec.ts @@ -0,0 +1,87 @@ +import { expect } from 'chai'; + +import { replacesNestedValues } from './replacesNestedValues'; + +describe('LDAP replacesNestedValues', () => { + it('should replace shallow values', () => { + const result = replacesNestedValues( + { + a: 1, + }, + 'a', + 2, + ); + expect(result).to.eql({ + a: 2, + }); + }); + + it('should replace undefined values', () => { + const result = replacesNestedValues({}, 'a', 2); + expect(result).to.eql({ + a: 2, + }); + }); + + it('should replace nested values', () => { + const result = replacesNestedValues( + { + a: { + b: 1, + }, + }, + 'a.b', + 2, + ); + expect(result).to.eql({ + a: { + b: 2, + }, + }); + }); + it('should replace undefined nested values', () => { + const result = replacesNestedValues( + { + a: {}, + }, + 'a.b', + 2, + ); + expect(result).to.eql({ + a: { + b: 2, + }, + }); + }); + + it('should fail if the value being replaced is not an object', () => { + expect(() => + replacesNestedValues( + { + a: [], + }, + 'a.b', + 2, + ), + ).to.throw(); + expect(() => + replacesNestedValues( + { + a: 1, + }, + 'a.b', + 2, + ), + ).to.throw(); + + expect(() => + replacesNestedValues( + { + a: { b: [] }, + }, + 'a.b', + 2, + ), + ).to.throw(); + }); +}); diff --git a/apps/meteor/ee/server/lib/ldap/replacesNestedValues.ts b/apps/meteor/ee/server/lib/ldap/replacesNestedValues.ts new file mode 100644 index 00000000000..c8af4be0cd7 --- /dev/null +++ b/apps/meteor/ee/server/lib/ldap/replacesNestedValues.ts @@ -0,0 +1,26 @@ +export const replacesNestedValues = (obj: Record<string, unknown>, key: string, value: unknown): Record<string, unknown> => { + const keys = key.split('.'); + const lastKey = keys.shift(); + + if (!lastKey) { + throw new Error(`Failed to assign custom field: ${key}`); + } + + if (keys.length && obj[lastKey] !== undefined && (typeof obj[lastKey] !== 'object' || Array.isArray(obj[lastKey]))) { + throw new Error(`Failed to assign custom field: ${key}`); + } + + if (keys.length === 0 && typeof obj[lastKey] === 'object') { + throw new Error(`Failed to assign custom field: ${key}`); + } + + return { + ...obj, + ...(keys.length === 0 && { + [lastKey]: value, + }), + ...(keys.length > 0 && { + [lastKey]: replacesNestedValues(obj[lastKey] as Record<string, unknown>, keys.join('.'), value), + }), + }; +}; -- GitLab From c931d9e92facb5ef4851c5490960edb3aefd0e79 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Wed, 5 Oct 2022 14:16:10 -0300 Subject: [PATCH 100/107] Bump version to 5.2.0-rc.4 --- .github/history.json | 22 ++++++++++++++++++++++ HISTORY.md | 16 ++++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/history.json b/.github/history.json index 63fba22c6a3..74c7b83c5c1 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94854,6 +94854,28 @@ ] } ] + }, + "5.2.0-rc.4": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26994", + "title": "Chore: break LDAP manager into smaller pieces to improve unit tests", + "userLogin": "ggazzo", + "milestone": "5.2.0", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 87b78ed80d8..725acc718d4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,22 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.4 +`2022-10-05 · 1 🔠· 2 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +<details> +<summary>🔠Minor changes</summary> + + +- Chore: break LDAP manager into smaller pieces to improve unit tests ([#26994](https://github.com/RocketChat/Rocket.Chat/pull/26994)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@ggazzo](https://github.com/ggazzo) +- [@tassoevan](https://github.com/tassoevan) + ## 5.2.0-rc.3 `2022-10-04 · 2 🔠· 3 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index b2196db29bd..76e3ffd179c 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.3 +ENV RC_VERSION 5.2.0-rc.4 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 1a36be98213..33f0cbf6f7e 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.3" + "version": "5.2.0-rc.4" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index ee5262ff0d5..6173436149f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.3", + "version": "5.2.0-rc.4", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 03015b450f5..826183ac8c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.3", + "version": "5.2.0-rc.4", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From fc71f8048c7b65718c7c3bcdaa134c5a822b54d2 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 7 Oct 2022 09:35:27 -0300 Subject: [PATCH 101/107] Regression: Cannot edit messages in some environments. (#27023) --- apps/meteor/app/lib/server/functions/updateMessage.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index d597f1a216e..9d18577b46c 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -1,6 +1,5 @@ import type { IMessage, IMessageEdited, IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import type { UpdateFilter } from 'mongodb'; import { Messages, Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; @@ -47,14 +46,13 @@ export const updateMessage = function (message: IMessage, user: IUser, originalM message = callbacks.run('beforeSaveMessage', message); const { _id, ...editedMessage } = message; - const unsetData: Partial<UpdateFilter<IMessage>> = {}; if (!editedMessage.msg) { - unsetData.md = 1; delete editedMessage.md; } - Messages.update({ _id }, { $set: editedMessage, $unset: unsetData }); + // do not send $unset if not defined. Can cause exceptions in certain mongo versions. + Messages.update({ _id }, { $set: editedMessage, ...(!editedMessage.md && { $unset: { md: 1 } }) }); const room = Rooms.findOneById(message.rid); -- GitLab From 113d0b42d6d6b7baf813ad439daeed4ed02086c0 Mon Sep 17 00:00:00 2001 From: Martin Schoeler <martin.schoeler@rocket.chat> Date: Fri, 7 Oct 2022 16:00:43 -0300 Subject: [PATCH 102/107] Regression: Double Table Cell Causing extra padding on Current Chats (#27008) Co-authored-by: Kevin Aleman <kaleman960@gmail.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../views/omnichannel/currentChats/CurrentChatsRoute.tsx | 6 +----- .../views/omnichannel/currentChats/RemoveChatButton.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 1510c1c757d..53f44a1fafc 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -173,11 +173,7 @@ const CurrentChatsRoute = (): ReactElement => { <GenericTableCell withTruncatedText data-qa='current-chats-cell-status'> {getStatusText(open, onHold)} </GenericTableCell> - {canRemoveClosedChats && !open && ( - <GenericTableCell withTruncatedText> - <RemoveChatButton _id={_id} /> - </GenericTableCell> - )} + {canRemoveClosedChats && !open && <RemoveChatButton _id={_id} />} </GenericTableRow> ); }, diff --git a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx index ffe23e08ba1..adfc49b096f 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/RemoveChatButton.tsx @@ -1,9 +1,10 @@ -import { Table, IconButton } from '@rocket.chat/fuselage'; +import { IconButton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC } from 'react'; import GenericModal from '../../../components/GenericModal'; +import { GenericTableCell } from '../../../components/GenericTable'; import { useRemoveCurrentChatMutation } from './hooks/useRemoveCurrentChatMutation'; const RemoveChatButton: FC<{ _id: string }> = ({ _id }) => { @@ -38,9 +39,9 @@ const RemoveChatButton: FC<{ _id: string }> = ({ _id }) => { }); return ( - <Table.Cell fontScale='p2' color='hint' withTruncatedText data-qa='current-chats-cell-delete'> + <GenericTableCell maxHeight='x36' fontScale='p2' color='hint' withTruncatedText data-qa='current-chats-cell-delete'> <IconButton small icon='trash' title={t('Remove')} disabled={removeCurrentChatMutation.isLoading} onClick={handleDelete} /> - </Table.Cell> + </GenericTableCell> ); }; -- GitLab From 404cfffe3fcee219579efce47ee0cab114162c69 Mon Sep 17 00:00:00 2001 From: Douglas Fabris <devfabris@gmail.com> Date: Fri, 7 Oct 2022 17:31:29 -0300 Subject: [PATCH 103/107] Regression: VideoConf Actions Reactivity in SidebarItem (#27009) Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> --- .../client/sidebar/RoomList/RoomList.tsx | 23 ++++++++----------- .../RoomList/{Row.tsx => RoomListRow.tsx} | 6 ++--- .../RoomList/SideBarItemTemplateWithData.tsx | 23 +++++++++++-------- 3 files changed, 27 insertions(+), 25 deletions(-) rename apps/meteor/client/sidebar/RoomList/{Row.tsx => RoomListRow.tsx} (91%) diff --git a/apps/meteor/client/sidebar/RoomList/RoomList.tsx b/apps/meteor/client/sidebar/RoomList/RoomList.tsx index d5ae9bb8266..da353718ff4 100644 --- a/apps/meteor/client/sidebar/RoomList/RoomList.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomList.tsx @@ -11,27 +11,22 @@ import { useRoomList } from '../hooks/useRoomList'; import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu'; import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor'; import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import Row from './Row'; +import RoomListRow from './RoomListRow'; import ScrollerWithCustomProps from './ScrollerWithCustomProps'; const computeItemKey = (index: number, room: IRoom): IRoom['_id'] | number => room._id || index; const RoomList = (): ReactElement => { - useSidebarPaletteColor(); - + const t = useTranslation(); + const isAnonymous = !useUserId(); + const roomsList = useRoomList(); + const avatarTemplate = useAvatarTemplate(); + const sideBarItemTemplate = useTemplateByViewMode(); const { ref } = useResizeObserver({ debounceDelay: 100 }); - const openedRoom = (useSession('openedRoom') as string) || ''; - const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode') || 'extended'; - const sideBarItemTemplate = useTemplateByViewMode(); - const avatarTemplate = useAvatarTemplate(); - const extended = sidebarViewMode === 'extended'; - const isAnonymous = !useUserId(); - - const t = useTranslation(); - const roomsList = useRoomList(); + const extended = sidebarViewMode === 'extended'; const itemData = useMemo( () => ({ extended, @@ -47,6 +42,8 @@ const RoomList = (): ReactElement => { usePreventDefault(ref); useShortcutOpenMenu(ref); + useSidebarPaletteColor(); + return ( <Box h='full' w='full' ref={ref}> <Virtuoso @@ -54,7 +51,7 @@ const RoomList = (): ReactElement => { data={roomsList} components={{ Scroller: ScrollerWithCustomProps }} computeItemKey={computeItemKey} - itemContent={(_, data): ReactElement => <Row data={itemData} item={data} />} + itemContent={(_, data): ReactElement => <RoomListRow data={itemData} item={data} />} /> </Box> ); diff --git a/apps/meteor/client/sidebar/RoomList/Row.tsx b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx similarity index 91% rename from apps/meteor/client/sidebar/RoomList/Row.tsx rename to apps/meteor/client/sidebar/RoomList/RoomListRow.tsx index 394c84f7783..955e185ce3e 100644 --- a/apps/meteor/client/sidebar/RoomList/Row.tsx +++ b/apps/meteor/client/sidebar/RoomList/RoomListRow.tsx @@ -25,7 +25,7 @@ type RoomListRowProps = { isAnonymous: boolean; }; -const Row = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRoom }): ReactElement => { +const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRoom }): ReactElement => { const { extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data; const acceptCall = useVideoConfAcceptCall(); @@ -62,9 +62,9 @@ const Row = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRo extended={extended} SideBarItemTemplate={SideBarItemTemplate} AvatarTemplate={AvatarTemplate} - videoConfActions={currentCall && videoConfActions} + videoConfActions={videoConfActions} /> ); }; -export default memo(Row); +export default memo(RoomListRow); diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 67889bc3a4f..263d4d75dd5 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -9,7 +9,7 @@ import { } from '@rocket.chat/core-typings'; import { Badge, Sidebar, SidebarItemAction } from '@rocket.chat/fuselage'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { AllHTMLAttributes, ComponentType, memo, ReactElement, ReactNode } from 'react'; +import React, { AllHTMLAttributes, ComponentType, memo, ReactElement, ReactNode, useMemo } from 'react'; import { RoomIcon } from '../../components/RoomIcon'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; @@ -115,6 +115,17 @@ function SideBarItemTemplateWithData({ </Sidebar.Item.Icon> ); + const actions = useMemo( + () => + videoConfActions && ( + <> + <SidebarItemAction onClick={videoConfActions.acceptCall} secondary success icon='phone' /> + <SidebarItemAction onClick={videoConfActions.rejectCall} secondary danger icon='phone-off' /> + </> + ), + [videoConfActions], + ); + const isQueued = isOmnichannelRoom(room) && room.status === 'queued'; const threadUnread = tunread.length > 0; @@ -151,14 +162,7 @@ function SideBarItemTemplateWithData({ style={style} badges={badges} avatar={AvatarTemplate && <AvatarTemplate {...room} />} - actions={ - videoConfActions && ( - <> - <SidebarItemAction onClick={videoConfActions.acceptCall} secondary success icon='phone' /> - <SidebarItemAction onClick={videoConfActions.rejectCall} secondary danger icon='phone-off' /> - </> - ) - } + actions={actions} menu={ !isAnonymous && !isQueued && @@ -195,6 +199,7 @@ const keys: (keyof RoomListRowProps)[] = [ 'AvatarTemplate', 't', 'sidebarViewMode', + 'videoConfActions', ]; // eslint-disable-next-line react/no-multi-comp -- GitLab From d6dd22a50cead8545421c57237a6bc1f6b47682d Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Mon, 10 Oct 2022 10:47:50 -0300 Subject: [PATCH 104/107] Bump version to 5.2.0-rc.5 --- .github/history.json | 45 +++++++++++++++++++++++++++ HISTORY.md | 27 ++++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/.github/history.json b/.github/history.json index 74c7b83c5c1..b55cb018cd7 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94876,6 +94876,51 @@ ] } ] + }, + "5.2.0-rc.5": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "27009", + "title": "Regression: VideoConf Actions Reactivity in SidebarItem", + "userLogin": "dougfabris", + "description": "Jira Issue: [VC-10]", + "milestone": "5.2.0", + "contributors": [ + "dougfabris", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "27008", + "title": "Regression: Double Table Cell Causing extra padding on Current Chats", + "userLogin": "MartinSchoeler", + "milestone": "5.2.0", + "contributors": [ + "MartinSchoeler", + "KevLehman", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "27023", + "title": "Regression: Cannot edit messages in some environments.", + "userLogin": "gabriellsh", + "description": "An empty `$unset` object was being used and causes exceptions in some mongo versions.", + "milestone": "5.2.0", + "contributors": [ + "gabriellsh" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 725acc718d4..c53ca3665f0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,33 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.5 +`2022-10-10 · 3 🔠· 5 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +<details> +<summary>🔠Minor changes</summary> + + +- Regression: Cannot edit messages in some environments. ([#27023](https://github.com/RocketChat/Rocket.Chat/pull/27023)) + + An empty `$unset` object was being used and causes exceptions in some mongo versions. + +- Regression: Double Table Cell Causing extra padding on Current Chats ([#27008](https://github.com/RocketChat/Rocket.Chat/pull/27008)) + +- Regression: VideoConf Actions Reactivity in SidebarItem ([#27009](https://github.com/RocketChat/Rocket.Chat/pull/27009)) + + Jira Issue: [VC-10] + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@dougfabris](https://github.com/dougfabris) +- [@gabriellsh](https://github.com/gabriellsh) +- [@tassoevan](https://github.com/tassoevan) + ## 5.2.0-rc.4 `2022-10-05 · 1 🔠· 2 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 76e3ffd179c..4f5c635e123 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.4 +ENV RC_VERSION 5.2.0-rc.5 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 33f0cbf6f7e..d81f85b3242 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.4" + "version": "5.2.0-rc.5" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 6173436149f..c9964c1f54a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.4", + "version": "5.2.0-rc.5", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 826183ac8c4..efc97571996 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.4", + "version": "5.2.0-rc.5", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From ff732f98c3f9e696f63b68ba97d4ba7aec891359 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 10 Oct 2022 15:50:52 -0300 Subject: [PATCH 105/107] Chore: Bump fuselage to latest (#27042) --- apps/meteor/ee/server/services/package.json | 10 +- apps/meteor/package.json | 30 +- ee/apps/account-service/package.json | 4 +- ee/apps/authorization-service/package.json | 4 +- ee/apps/ddp-streamer/package.json | 4 +- ee/apps/presence-service/package.json | 4 +- ee/apps/stream-hub-service/package.json | 4 +- packages/core-typings/package.json | 6 +- packages/fuselage-ui-kit/package.json | 12 +- packages/gazzodown/package.json | 10 +- packages/livechat/package.json | 6 +- packages/rest-typings/package.json | 4 +- packages/ui-client/package.json | 8 +- packages/ui-composer/package.json | 4 +- packages/ui-contexts/package.json | 4 +- packages/ui-video-conf/package.json | 8 +- yarn.lock | 361 ++++++++------------ 17 files changed, 209 insertions(+), 274 deletions(-) diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 277798c838e..3261212374b 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -20,13 +20,13 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", - "@rocket.chat/message-parser": "next", + "@rocket.chat/emitter": "0.31.19", + "@rocket.chat/message-parser": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "next", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/string-helpers": "0.31.19", + "@rocket.chat/ui-kit": "0.31.19", "ajv": "^8.11.0", "bcrypt": "^5.0.1", "body-parser": "^1.20.0", @@ -50,7 +50,7 @@ "ws": "^8.8.1" }, "devDependencies": { - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "0.31.19", "@types/cookie": "^0.5.1", "@types/cookie-parser": "^1.4.3", "@types/ejson": "^2.2.0", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 0d50c3493ab..d824d4290ef 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -202,35 +202,35 @@ "@rocket.chat/apps-engine": "1.35.0-alpha.46", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/css-in-js": "next", - "@rocket.chat/emitter": "next", + "@rocket.chat/css-in-js": "0.31.19", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/fuselage-polyfills": "next", - "@rocket.chat/fuselage-toastbar": "next", - "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/fuselage-ui-kit": "next", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/fuselage-hooks": "0.31.19", + "@rocket.chat/fuselage-polyfills": "0.31.19", + "@rocket.chat/fuselage-toastbar": "0.31.19", + "@rocket.chat/fuselage-tokens": "0.31.19", + "@rocket.chat/fuselage-ui-kit": "0.31.19", "@rocket.chat/gazzodown": "workspace:^", - "@rocket.chat/icons": "next", + "@rocket.chat/icons": "0.31.19", "@rocket.chat/layout": "next", - "@rocket.chat/logo": "next", - "@rocket.chat/memo": "next", - "@rocket.chat/message-parser": "next", + "@rocket.chat/logo": "0.31.19", + "@rocket.chat/memo": "0.31.19", + "@rocket.chat/message-parser": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", - "@rocket.chat/onboarding-ui": "next", + "@rocket.chat/onboarding-ui": "0.31.19", "@rocket.chat/poplib": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-composer": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "0.31.19", "@rocket.chat/ui-video-conf": "workspace:^", "@slack/rtm-api": "^6.0.0", "@tanstack/react-query": "^4.0.10", diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b5650aa8018..b89a67809f2 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -16,11 +16,11 @@ "author": "Rocket.Chat", "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "@types/node": "^14.18.21", "bcrypt": "^5.0.1", "ejson": "^2.2.2", diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 824768de3cf..721e4a5d1ca 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -16,11 +16,11 @@ "author": "Rocket.Chat", "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "@types/node": "^14.18.21", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 2cdb260f523..b43c32d0f05 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -17,11 +17,11 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "colorette": "^1.4.0", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index c49bba3bc1c..6c4001af86c 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -16,11 +16,11 @@ "author": "Rocket.Chat", "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/presence": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "@types/node": "^14.18.21", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 6634c1d8dc6..dc3b9ab4873 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -16,10 +16,10 @@ "author": "Rocket.Chat", "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", + "@rocket.chat/emitter": "0.31.19", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", - "@rocket.chat/string-helpers": "next", + "@rocket.chat/string-helpers": "0.31.19", "@types/node": "^14.18.21", "ejson": "^2.2.2", "eventemitter3": "^4.0.7", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 7faf5cf4e29..fe4caee7521 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -23,8 +23,8 @@ ], "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", - "@rocket.chat/icons": "next", - "@rocket.chat/message-parser": "next", - "@rocket.chat/ui-kit": "next" + "@rocket.chat/icons": "0.31.19", + "@rocket.chat/message-parser": "0.31.19", + "@rocket.chat/ui-kit": "0.31.19" } } diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index b89ffc23b27..1f357bc7534 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -49,12 +49,12 @@ "devDependencies": { "@rocket.chat/apps-engine": "~1.30.0", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/fuselage-polyfills": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/fuselage-hooks": "0.31.19", + "@rocket.chat/fuselage-polyfills": "0.31.19", + "@rocket.chat/icons": "0.31.19", "@rocket.chat/prettier-config": "next", - "@rocket.chat/styled": "next", + "@rocket.chat/styled": "0.31.19", "@storybook/addon-essentials": "~6.5.10", "@storybook/addons": "~6.5.10", "@storybook/builder-webpack5": "~6.5.10", @@ -78,7 +78,7 @@ "webpack": "~5.68.0" }, "dependencies": { - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "0.31.19", "tslib": "^2.3.1" } } diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b1e9ce93dfb..02166d7befe 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,11 +6,11 @@ "@babel/core": "^7.18.9", "@mdx-js/react": "^1.6.22", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/message-parser": "next", - "@rocket.chat/styled": "next", + "@rocket.chat/css-in-js": "0.31.19", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/fuselage-tokens": "0.31.19", + "@rocket.chat/message-parser": "0.31.19", + "@rocket.chat/styled": "0.31.19", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", "@storybook/addon-actions": "~6.5.10", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index d87419d4658..6ca48ad303b 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -26,8 +26,8 @@ "@babel/eslint-parser": "^7.18.9", "@babel/preset-env": "^7.18.9", "@rocket.chat/eslint-config": "^0.4.0", - "@rocket.chat/fuselage-tokens": "next", - "@rocket.chat/logo": "next", + "@rocket.chat/fuselage-tokens": "0.31.19", + "@rocket.chat/logo": "0.31.19", "@storybook/addon-actions": "~6.5.10", "@storybook/addon-backgrounds": "~6.5.10", "@storybook/addon-essentials": "~6.5.10", @@ -87,7 +87,7 @@ }, "dependencies": { "@rocket.chat/sdk": "^1.0.0-alpha.42", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/ui-kit": "0.31.19", "crypto-js": "^4.1.1", "css-vars-ponyfill": "^2.4.7", "date-fns": "^2.15.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 0b8648c0d62..30e30f16a59 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -26,8 +26,8 @@ "dependencies": { "@rocket.chat/apps-engine": "^1.32.0", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/message-parser": "next", - "@rocket.chat/ui-kit": "next", + "@rocket.chat/message-parser": "0.31.19", + "@rocket.chat/ui-kit": "0.31.19", "ajv": "^8.11.0" } } diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index ce17f2da0d9..4aa5d5542d5 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -3,10 +3,10 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/css-in-js": "0.31.19", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/fuselage-hooks": "0.31.19", + "@rocket.chat/icons": "0.31.19", "@rocket.chat/ui-contexts": "workspace:~", "@storybook/addon-actions": "~6.5.10", "@storybook/addon-docs": "~6.5.10", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 6cd3858d674..a3b72229b6e 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -5,8 +5,8 @@ "devDependencies": { "@babel/core": "~7.18.13", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "next", - "@rocket.chat/icons": "next", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/icons": "0.31.19", "@storybook/addon-actions": "~6.5.12", "@storybook/addon-docs": "~6.5.12", "@storybook/addon-essentials": "~6.5.12", diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 7858794225d..b48c76c6b03 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -4,8 +4,8 @@ "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/emitter": "next", - "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/emitter": "0.31.19", + "@rocket.chat/fuselage-hooks": "0.31.19", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "^27.4.1", "@types/react": "^17.0.47", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 7bab37906fa..9a0a429baa0 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -3,11 +3,11 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@rocket.chat/css-in-js": "next", + "@rocket.chat/css-in-js": "0.31.19", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "next", - "@rocket.chat/fuselage-hooks": "next", - "@rocket.chat/styled": "next", + "@rocket.chat/fuselage": "0.31.19", + "@rocket.chat/fuselage-hooks": "0.31.19", + "@rocket.chat/styled": "0.31.19", "@types/jest": "^27.4.1", "eslint": "^8.20.0", "eslint-plugin-react": "^7.30.1", diff --git a/yarn.lock b/yarn.lock index 2b184725228..3158cedad7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4956,12 +4956,12 @@ __metadata: resolution: "@rocket.chat/account-service@workspace:ee/apps/account-service" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@types/bcrypt": ^5.0.0 "@types/eslint": ^8 "@types/node": ^14.18.21 @@ -5069,12 +5069,12 @@ __metadata: resolution: "@rocket.chat/authorization-service@workspace:ee/apps/authorization-service" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@types/eslint": ^8 "@types/node": ^14.18.21 "@types/polka": ^0.5.4 @@ -5112,9 +5112,9 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": next - "@rocket.chat/message-parser": next - "@rocket.chat/ui-kit": next + "@rocket.chat/icons": 0.31.19 + "@rocket.chat/message-parser": 0.31.19 + "@rocket.chat/ui-kit": 0.31.19 eslint: ^8.20.0 mongodb: ^4.3.1 prettier: ^2.7.1 @@ -5122,47 +5122,25 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.19-dev.19": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.19" +"@rocket.chat/css-in-js@npm:0.31.19, @rocket.chat/css-in-js@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/css-in-js@npm:0.31.19" dependencies: "@emotion/hash": ^0.8.0 - "@rocket.chat/css-supports": ~0.31.19-dev.19 - "@rocket.chat/memo": ~0.31.19-dev.19 - "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.19 + "@rocket.chat/css-supports": ^0.31.19 + "@rocket.chat/memo": ^0.31.19 + "@rocket.chat/stylis-logical-props-middleware": ^0.31.19 stylis: ~4.0.13 - checksum: 1f0a1de2da1f7a50526b5f3a5bb33dc9cdaf53f450d6c6403c3460e15f308bd6e2eeb5e4ca219f909b99b8f94a1b1ded6c45578b424d3a4f5029820ed4880d1f + checksum: 112fdad44db62096a52f2923020d41f54da857d16fd16f6d1c17b1f39cc56b6234c8c95620407930e069b10aaa390146bae5985d0cedac7bd2dacf794ccf99af languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:~0.31.19-dev.26": - version: 0.31.19-dev.26 - resolution: "@rocket.chat/css-in-js@npm:0.31.19-dev.26" +"@rocket.chat/css-supports@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/css-supports@npm:0.31.19" dependencies: - "@emotion/hash": ^0.8.0 - "@rocket.chat/css-supports": ~0.31.19-dev.26 - "@rocket.chat/memo": ~0.31.19-dev.26 - "@rocket.chat/stylis-logical-props-middleware": ~0.31.19-dev.26 - stylis: ~4.0.13 - checksum: a89128e73179c98d9b78f26cfe1ee26ec07581427b056f2142373a15c56204e3d489f04c50abf35baff83109848c61a1d286320e0dfe683d9ea80c0ffbaba4c4 - languageName: node - linkType: hard - -"@rocket.chat/css-supports@npm:~0.31.19-dev.19": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.19" - dependencies: - "@rocket.chat/memo": ~0.31.19-dev.19 - checksum: ea8aaab14336894d028bb9ef86db2efa7df9252c403b6faac50276708431a2ffe29150acacf836f1e4aa05a2ac87c42a8e5821c0903bd42158e4c9adfc71e226 - languageName: node - linkType: hard - -"@rocket.chat/css-supports@npm:~0.31.19-dev.26": - version: 0.31.19-dev.26 - resolution: "@rocket.chat/css-supports@npm:0.31.19-dev.26" - dependencies: - "@rocket.chat/memo": ~0.31.19-dev.26 - checksum: c890f2b8b32f77dba62569e5ca3d5e79e0d50a4b3678334afdaa8213385c1b2a9481033aaab263905abe0c833fb1f26e919f384fba714030fa8a99226e87f3d8 + "@rocket.chat/memo": ^0.31.19 + checksum: cdf333edab8fccf3574422c6d6bce55fb4149cd4f07867b24aceadf07d460cc547686014da38319f7e9c4d182c368a3828e405ef0d2611c9a975f030b49f7812 languageName: node linkType: hard @@ -5172,12 +5150,12 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@types/ejson": ^2.2.0 "@types/eslint": ^8 "@types/meteor": 2.7.1 @@ -5207,10 +5185,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/emitter@npm:next": - version: 0.31.19-dev.7 - resolution: "@rocket.chat/emitter@npm:0.31.19-dev.7" - checksum: 328b0c061a831efe66a66f36189a9039ff5bfb9dd95d2a6d63cdc89798b8abd8f9de786b041b261c913c84190c410e39a2304cd909bc45f619e67ba82407c522 +"@rocket.chat/emitter@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/emitter@npm:0.31.19" + checksum: 4479dc692f9da294927c0f4c01c39b2651885bb75cea010a14e5dd58247a68daae23c27d8bf79f811e9b26eb43041f108435faf82d4476b58fe373bf2d012118 languageName: node linkType: hard @@ -5313,21 +5291,21 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-hooks@npm:next, @rocket.chat/fuselage-hooks@npm:~0.32.0-dev.64": - version: 0.32.0-dev.64 - resolution: "@rocket.chat/fuselage-hooks@npm:0.32.0-dev.64" +"@rocket.chat/fuselage-hooks@npm:0.31.19, @rocket.chat/fuselage-hooks@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage-hooks@npm:0.31.19" dependencies: use-sync-external-store: ~1.2.0 peerDependencies: "@rocket.chat/fuselage-tokens": "*" react: ^17.0.2 - checksum: 0b474994184e05a6c0d3be4f72140fd6db12066c6a57bced560887e826a9da302557cf6690f2cf3a50a83664e657633981bebb291033f6fc4ff48a8e90b6dd46 + checksum: 13329a809eba50bb12d9e5e64051cbee24e4c9b4149dfa2b68288d91904582e18c145c157ccf4cf9d5422b4339dfcc723da75d1ffde980b5a10f7ebf04c8abf1 languageName: node linkType: hard -"@rocket.chat/fuselage-polyfills@npm:next": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/fuselage-polyfills@npm:0.31.19-dev.19" +"@rocket.chat/fuselage-polyfills@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage-polyfills@npm:0.31.19" dependencies: "@juggle/resize-observer": ^3.3.1 clipboard-polyfill: ^3.0.3 @@ -5335,13 +5313,13 @@ __metadata: focus-visible: ^5.2.0 focus-within-polyfill: ^5.2.1 new-event-polyfill: ^1.0.1 - checksum: 3ae50bd495c88f6ff74c88cc4ce9e3fa9d31be08bce94ee7769323c05aa544af8fc494088d31f2049eab8c953b55f0281823424c7f853cff6ce5427f9989092a + checksum: faa1703abda9e54f3a9a4663be375851e881b5729a7581a5edb7b5691e7ceda15540fc5398b15e2063a4b78667ea97c6db59a02cc233ea8462508121cca5fa5f languageName: node linkType: hard -"@rocket.chat/fuselage-toastbar@npm:next": - version: 0.32.0-dev.125 - resolution: "@rocket.chat/fuselage-toastbar@npm:0.32.0-dev.125" +"@rocket.chat/fuselage-toastbar@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage-toastbar@npm:0.31.19" peerDependencies: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" @@ -5349,29 +5327,22 @@ __metadata: "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 - checksum: 73d447a894080af1d174d6ee89f12b4b6c52b668e76bcd008b225b0aec16c9247b07553638e6c21cb5437dd8dcb83d6a051ce1f3ff6f2e7b26b2b8eb2b9e1efd - languageName: node - linkType: hard - -"@rocket.chat/fuselage-tokens@npm:next": - version: 0.32.0-dev.101 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.101" - checksum: 516aeecbfb84311bf819a867447a189467c7058cf65affe1e4b43ebde48b1d7db3714cbbcb4d1e8f4cc45369338f643ecd6d9e3d8b7b076ec12319492e1d8979 + checksum: b3d4704d38ba19f7ca3535a17550afc6e71dca829689ef75c736611bbf6715975b5da72c316696af29cd8173ff838ba737dbcbf580f39e521217ed92b3e71ff4 languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.108": - version: 0.32.0-dev.108 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.108" - checksum: 40a85e19eea4359c56d16a253b30bcb10c85be3e117e836afeb57bd29596a809d4820278801a0b1dbf6dc8c38279006fcc8cfab761a724de974b6d0e14c057fc +"@rocket.chat/fuselage-tokens@npm:0.31.19, @rocket.chat/fuselage-tokens@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage-tokens@npm:0.31.19" + checksum: 25252b23da5329bcf2b5193be9a5edbd60e2ecea7c65647924e89d5c72a44104b8ef16f4cfa348c03c28a689b2e324fb6ba566fd415f05d75a178dd926d8430f languageName: node linkType: hard -"@rocket.chat/fuselage-ui-kit@npm:next": - version: 0.32.0-dev.104 - resolution: "@rocket.chat/fuselage-ui-kit@npm:0.32.0-dev.104" +"@rocket.chat/fuselage-ui-kit@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage-ui-kit@npm:0.31.19" dependencies: - "@rocket.chat/ui-kit": ~0.32.0-dev.89 + "@rocket.chat/ui-kit": ^0.31.19 tslib: ^2.3.1 peerDependencies: "@rocket.chat/fuselage": "*" @@ -5381,7 +5352,7 @@ __metadata: "@rocket.chat/styled": "*" react: ^17.0.2 react-dom: ^17.0.2 - checksum: a83fb2f2d88bde7128de931ee26a60e2b3b332bf666b6195c72a7222f6b61130f7afd692f63dbfc80562c64295ca6bc47050d7a2dc62a1b7fb1c492b0c554af1 + checksum: a5f0a5e8de7c53b00aee091b8f27754504b08c4fe02c19c9f44635baa3a0a9aa284a657cd9b5eaf4f9054da5e4faebb532f7ae38f66df1dfe873a857067cb7a6 languageName: node linkType: hard @@ -5391,13 +5362,13 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ~1.30.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": next - "@rocket.chat/fuselage-polyfills": next - "@rocket.chat/icons": next + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/fuselage-hooks": 0.31.19 + "@rocket.chat/fuselage-polyfills": 0.31.19 + "@rocket.chat/icons": 0.31.19 "@rocket.chat/prettier-config": next - "@rocket.chat/styled": next - "@rocket.chat/ui-kit": next + "@rocket.chat/styled": 0.31.19 + "@rocket.chat/ui-kit": 0.31.19 "@storybook/addon-essentials": ~6.5.10 "@storybook/addons": ~6.5.10 "@storybook/builder-webpack5": ~6.5.10 @@ -5431,15 +5402,15 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:next": - version: 0.32.0-dev.158 - resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.158" +"@rocket.chat/fuselage@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/fuselage@npm:0.31.19" dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.26 - "@rocket.chat/css-supports": ~0.31.19-dev.26 - "@rocket.chat/fuselage-tokens": ~0.32.0-dev.108 - "@rocket.chat/memo": ~0.31.19-dev.26 - "@rocket.chat/styled": ~0.31.19-dev.26 + "@rocket.chat/css-in-js": ^0.31.19 + "@rocket.chat/css-supports": ^0.31.19 + "@rocket.chat/fuselage-tokens": ^0.31.19 + "@rocket.chat/memo": ^0.31.19 + "@rocket.chat/styled": ^0.31.19 invariant: ^2.2.4 react-aria: ~3.19.0 react-keyed-flatten-children: ^1.3.0 @@ -5451,7 +5422,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: bd617d89fb2477a068bec33421457b93b5f97066c796bd621e8e358608d893fe41cbfaba9f7ebfea3435d98756ed473d805b1bad3d231c7ffc0699b5242fdd55 + checksum: 8c627d50fce3dfe5c88aec447ac6c411a2f3f8a529bd843f9b28093ff2374f8d6e518733050bea2c526f5e91206b1dc02547a6d3e1a791af036d7ddecb67b97a languageName: node linkType: hard @@ -5462,11 +5433,11 @@ __metadata: "@babel/core": ^7.18.9 "@mdx-js/react": ^1.6.22 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-tokens": next - "@rocket.chat/message-parser": next - "@rocket.chat/styled": next + "@rocket.chat/css-in-js": 0.31.19 + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/fuselage-tokens": 0.31.19 + "@rocket.chat/message-parser": 0.31.19 + "@rocket.chat/styled": 0.31.19 "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" "@storybook/addon-actions": ~6.5.10 @@ -5516,10 +5487,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:next": - version: 0.32.0-dev.140 - resolution: "@rocket.chat/icons@npm:0.32.0-dev.140" - checksum: 9da9f6537268a49b814879b2abaaf93de05d42009e023459cf3c2d5966564910ae1ea83a2ccfe6723cfb8077546534d2d281418963d27daf5a58a4ee45029042 +"@rocket.chat/icons@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/icons@npm:0.31.19" + checksum: 1a0fc681baa1b41c67d2c4c5ebcb3db0120c7341f9e398a6f1da320e0fe6932a83d4ef68fb7e5bab7fa6dcd0f495fd2f032e3586c98567ad6ba726f6af8c07ec languageName: node linkType: hard @@ -5542,10 +5513,10 @@ __metadata: "@babel/eslint-parser": ^7.18.9 "@babel/preset-env": ^7.18.9 "@rocket.chat/eslint-config": ^0.4.0 - "@rocket.chat/fuselage-tokens": next - "@rocket.chat/logo": next + "@rocket.chat/fuselage-tokens": 0.31.19 + "@rocket.chat/logo": 0.31.19 "@rocket.chat/sdk": ^1.0.0-alpha.42 - "@rocket.chat/ui-kit": next + "@rocket.chat/ui-kit": 0.31.19 "@storybook/addon-actions": ~6.5.10 "@storybook/addon-backgrounds": ~6.5.10 "@storybook/addon-essentials": ~6.5.10 @@ -5618,38 +5589,31 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/logo@npm:next": - version: 0.32.0-dev.101 - resolution: "@rocket.chat/logo@npm:0.32.0-dev.101" +"@rocket.chat/logo@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/logo@npm:0.31.19" dependencies: - "@rocket.chat/fuselage-hooks": ~0.32.0-dev.64 - "@rocket.chat/styled": ~0.31.19-dev.19 + "@rocket.chat/fuselage-hooks": ^0.31.19 + "@rocket.chat/styled": ^0.31.19 tslib: ^2.3.1 peerDependencies: react: 17.0.2 react-dom: 17.0.2 - checksum: 857dead0323356f28a66d525331a7485d8401c896e7ac4bf8ab6c481cd6bbd4b3a1194079e48c48fa87bca93b9b2c0578e13e218ab6e0babc38e5fab89bb0a06 - languageName: node - linkType: hard - -"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.19-dev.19": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/memo@npm:0.31.19-dev.19" - checksum: 90d66af5499c6d49ff64a695faaebf8ed5f073c78472ff8f5f3432697fc3ef6065914ac302f64cb88d840d0816cba7d0148461bd15157a1678f886fe3e5d7adf + checksum: f29176cd4571f1db26aeab9cc08408fa29f620659c48758b5c19090857ecf05705e001d5be61d95303fa76d7113f6fd3f522427a90daa9552598204e6dfb572c languageName: node linkType: hard -"@rocket.chat/memo@npm:~0.31.19-dev.26": - version: 0.31.19-dev.26 - resolution: "@rocket.chat/memo@npm:0.31.19-dev.26" - checksum: 438059e30795269ab461fd557a9efbad956cbe93624ff5927d3e3abd9132d7cb6b1ac379b4f28efec11c87c34a145a168167e3d96cd111a380aaa23565874c99 +"@rocket.chat/memo@npm:0.31.19, @rocket.chat/memo@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/memo@npm:0.31.19" + checksum: 6d0341b8dbe80aef421914277d2dc3100efaac9fb6296f8fab3dd885dad40c9b211ced7b8e6dcbb7c9b05732f676bbc767585be20ab306a6234a7a0c191886b9 languageName: node linkType: hard -"@rocket.chat/message-parser@npm:next": - version: 0.32.0-dev.99 - resolution: "@rocket.chat/message-parser@npm:0.32.0-dev.99" - checksum: 92f08cc2f04e995030a47e5de871eb6b1cb36e45253a63ea812d2adc331d9fba9dc68ca484a87cc010bea0e7b37d877f35fe86db6b809d2f008aee64b72c5d7e +"@rocket.chat/message-parser@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/message-parser@npm:0.31.19" + checksum: 648f4dae31ac3ba0aa93e9b030a4cf2df9b59bc212027daff5755ad6ed15632401a3fa56831bb98627cedeae18d5eab499e890cd4331d634eb6def52595b9431 languageName: node linkType: hard @@ -5679,37 +5643,37 @@ __metadata: "@rocket.chat/apps-engine": 1.35.0-alpha.46 "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/css-in-js": next - "@rocket.chat/emitter": next + "@rocket.chat/css-in-js": 0.31.19 + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 - "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": next - "@rocket.chat/fuselage-polyfills": next - "@rocket.chat/fuselage-toastbar": next - "@rocket.chat/fuselage-tokens": next - "@rocket.chat/fuselage-ui-kit": next + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/fuselage-hooks": 0.31.19 + "@rocket.chat/fuselage-polyfills": 0.31.19 + "@rocket.chat/fuselage-toastbar": 0.31.19 + "@rocket.chat/fuselage-tokens": 0.31.19 + "@rocket.chat/fuselage-ui-kit": 0.31.19 "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": next + "@rocket.chat/icons": 0.31.19 "@rocket.chat/layout": next "@rocket.chat/livechat": "workspace:^" - "@rocket.chat/logo": next - "@rocket.chat/memo": next - "@rocket.chat/message-parser": next + "@rocket.chat/logo": 0.31.19 + "@rocket.chat/memo": 0.31.19 + "@rocket.chat/message-parser": 0.31.19 "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 - "@rocket.chat/onboarding-ui": next + "@rocket.chat/onboarding-ui": 0.31.19 "@rocket.chat/poplib": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-composer": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" - "@rocket.chat/ui-kit": next + "@rocket.chat/ui-kit": 0.31.19 "@rocket.chat/ui-video-conf": "workspace:^" "@settlin/spacebars-loader": ^1.0.9 "@slack/rtm-api": ^6.0.0 @@ -6018,9 +5982,9 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/onboarding-ui@npm:next": - version: 0.32.0-dev.151 - resolution: "@rocket.chat/onboarding-ui@npm:0.32.0-dev.151" +"@rocket.chat/onboarding-ui@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/onboarding-ui@npm:0.31.19" dependencies: i18next: ~21.6.11 react-hook-form: ~7.27.0 @@ -6036,7 +6000,7 @@ __metadata: react: 17.0.2 react-dom: 17.0.2 react-i18next: ~11.15.4 - checksum: 60bc5b214020138de8991315342fe0ee8fcfbc5d08479ec37962953b8bc6cf800618967792bdf4c6c7095e1c6924419c7deb746f00b3e11668ec0ebaa0ea2564 + checksum: 0a39c168d0617afc48e3f22742554d3a7f3f0142ed21e2cebfd2df4c1897ddd6066e64c01d1cb0261c0c3805842d16f41d972b6d75399836db0808e8d8cccccc languageName: node linkType: hard @@ -6057,12 +6021,12 @@ __metadata: resolution: "@rocket.chat/presence-service@workspace:ee/apps/presence-service" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/presence": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@types/eslint": ^8 "@types/node": ^14.18.21 "@types/polka": ^0.5.4 @@ -6117,8 +6081,8 @@ __metadata: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/message-parser": next - "@rocket.chat/ui-kit": next + "@rocket.chat/message-parser": 0.31.19 + "@rocket.chat/ui-kit": 0.31.19 "@types/jest": ^27.4.1 ajv: ^8.11.0 eslint: ^8.20.0 @@ -6147,11 +6111,11 @@ __metadata: resolution: "@rocket.chat/stream-hub-service@workspace:ee/apps/stream-hub-service" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next + "@rocket.chat/emitter": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" - "@rocket.chat/string-helpers": next + "@rocket.chat/string-helpers": 0.31.19 "@types/bcrypt": ^5.0.0 "@types/eslint": ^8 "@types/node": ^14.18.21 @@ -6171,56 +6135,34 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/string-helpers@npm:next": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/string-helpers@npm:0.31.19-dev.19" +"@rocket.chat/string-helpers@npm:0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/string-helpers@npm:0.31.19" dependencies: tslib: ^2.3.1 - checksum: 9546a53512c67367dda6c9afd0eac8a2d5eda6bbd5a590b32359223954d863a32639249fab6845c1bf16afb549d71835eef33bea89b7d347130e585414d11dbc + checksum: f969599a460d833698d5667caec5ae64fe59a679ecab9ce5abccc262a1b4611dcefc6eaf3343c98f30e87d01f058a41bfac1c8cc47a9eb22c9aba23b7d680f56 languageName: node linkType: hard -"@rocket.chat/styled@npm:next, @rocket.chat/styled@npm:~0.31.19-dev.19": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/styled@npm:0.31.19-dev.19" +"@rocket.chat/styled@npm:0.31.19, @rocket.chat/styled@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/styled@npm:0.31.19" dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.19 + "@rocket.chat/css-in-js": ^0.31.19 tslib: ^2.3.1 - checksum: 5f31d307c126aa003581fb6d2a3ac57d883268444bc02c7c9c4222f24747dadbbf84586b3fe28628f62e2f177311e0fc4a977e27fece162aa336aa53285c4596 + checksum: f6221bbe57b724cb79790d61c705873c7be08667a9c122315d21e2f2d6adc0fcf81becdf87cf838f9ee8a801b0497ca0d6a1f9e5254a7915ce30dab701598dbd languageName: node linkType: hard -"@rocket.chat/styled@npm:~0.31.19-dev.26": - version: 0.31.19-dev.26 - resolution: "@rocket.chat/styled@npm:0.31.19-dev.26" +"@rocket.chat/stylis-logical-props-middleware@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19" dependencies: - "@rocket.chat/css-in-js": ~0.31.19-dev.26 - tslib: ^2.3.1 - checksum: 43fe29955169450a0669905ccfcdedecca03cc3e6277be3faeb85b38332afb4df83736da7c5501d452ce869aafd4527af5bacb284948bb5fa83f669f54656142 - languageName: node - linkType: hard - -"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.19": - version: 0.31.19-dev.19 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.19" - dependencies: - "@rocket.chat/css-supports": ~0.31.19-dev.19 + "@rocket.chat/css-supports": ^0.31.19 tslib: ^2.3.1 peerDependencies: stylis: 4.0.10 - checksum: 9e78c7411de4c1f52b842a35c5060e8122814ccbcc5302878bbab5201baa5a6521478a0a70f31e7747b3782960d35c2f8ce39dee212e4270db2ab1b019766bce - languageName: node - linkType: hard - -"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.19-dev.26": - version: 0.31.19-dev.26 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.19-dev.26" - dependencies: - "@rocket.chat/css-supports": ~0.31.19-dev.26 - tslib: ^2.3.1 - peerDependencies: - stylis: 4.0.10 - checksum: ae845d95833790c60156114878691585de8c24e749f6f52742087584e4ae46b8a364c2ef5a29a7f5cde99de01013961c7475e162daf49b4160b6e81fc1e573b6 + checksum: dca6467fbe636cc10fa8298e65ae3052eb4da5cc803d4e77fee5e9d2aa806b65fa01bc82edb9f17e5defbcf49e3d4f0365f4a01b241cd3c1e10c187cc8777de6 languageName: node linkType: hard @@ -6228,10 +6170,10 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" dependencies: - "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": next - "@rocket.chat/icons": next + "@rocket.chat/css-in-js": 0.31.19 + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/fuselage-hooks": 0.31.19 + "@rocket.chat/icons": 0.31.19 "@rocket.chat/ui-contexts": "workspace:~" "@storybook/addon-actions": ~6.5.10 "@storybook/addon-docs": ~6.5.10 @@ -6277,8 +6219,8 @@ __metadata: dependencies: "@babel/core": ~7.18.13 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": next - "@rocket.chat/icons": next + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/icons": 0.31.19 "@storybook/addon-actions": ~6.5.12 "@storybook/addon-docs": ~6.5.12 "@storybook/addon-essentials": ~6.5.12 @@ -6308,8 +6250,8 @@ __metadata: resolution: "@rocket.chat/ui-contexts@workspace:packages/ui-contexts" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next - "@rocket.chat/fuselage-hooks": next + "@rocket.chat/emitter": 0.31.19 + "@rocket.chat/fuselage-hooks": 0.31.19 "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ^27.4.1 "@types/react": ^17.0.47 @@ -6331,17 +6273,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/ui-kit@npm:next": - version: 0.32.0-dev.86 - resolution: "@rocket.chat/ui-kit@npm:0.32.0-dev.86" - checksum: 234a8a384cdfd881074bfd357de7f3805a6af1798cc73d66fd89735abad7d6d0414f1bbe674cc805c0b8c6cf897364b257027c383cc3e8be4bb26207485332a7 - languageName: node - linkType: hard - -"@rocket.chat/ui-kit@npm:~0.32.0-dev.89": - version: 0.32.0-dev.89 - resolution: "@rocket.chat/ui-kit@npm:0.32.0-dev.89" - checksum: 20255ca3b62e288d98eb454375c91c0a494e6e27357f33a61357d6708e8dffdce815c746da03dffef3fc21e04c26ecd53e720a82332f33a532e28a1ac5ac60f4 +"@rocket.chat/ui-kit@npm:0.31.19, @rocket.chat/ui-kit@npm:^0.31.19": + version: 0.31.19 + resolution: "@rocket.chat/ui-kit@npm:0.31.19" + checksum: 4c837ca9e56b0d20875dac1fa0c29c0cfb9d8d088f7f75bb355fc8ad04ec670b0c19f43dd6459510b666ef173bcf1a3d913bdb3d82e052bceefd1e2a54c5f19b languageName: node linkType: hard @@ -6349,11 +6284,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-video-conf@workspace:packages/ui-video-conf" dependencies: - "@rocket.chat/css-in-js": next + "@rocket.chat/css-in-js": 0.31.19 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": next - "@rocket.chat/fuselage-hooks": next - "@rocket.chat/styled": next + "@rocket.chat/fuselage": 0.31.19 + "@rocket.chat/fuselage-hooks": 0.31.19 + "@rocket.chat/styled": 0.31.19 "@types/jest": ^27.4.1 eslint: ^8.20.0 eslint-plugin-react: ^7.30.1 @@ -23182,7 +23117,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 + checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d languageName: node linkType: hard @@ -30498,14 +30433,14 @@ __metadata: dependencies: "@rocket.chat/apps-engine": ^1.32.0 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/emitter": next - "@rocket.chat/icons": next - "@rocket.chat/message-parser": next + "@rocket.chat/emitter": 0.31.19 + "@rocket.chat/icons": 0.31.19 + "@rocket.chat/message-parser": 0.31.19 "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@rocket.chat/string-helpers": next - "@rocket.chat/ui-kit": next + "@rocket.chat/string-helpers": 0.31.19 + "@rocket.chat/ui-kit": 0.31.19 "@types/cookie": ^0.5.1 "@types/cookie-parser": ^1.4.3 "@types/ejson": ^2.2.0 -- GitLab From e5de91a114dc711d66d4c820d956f97f9fb632c0 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Mon, 10 Oct 2022 15:52:17 -0300 Subject: [PATCH 106/107] Bump version to 5.2.0-rc.6 --- .github/history.json | 20 ++++++++++++++++++++ HISTORY.md | 15 +++++++++++++++ apps/meteor/.docker/Dockerfile.rhel | 2 +- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- 6 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.github/history.json b/.github/history.json index b55cb018cd7..3601346c33a 100644 --- a/.github/history.json +++ b/.github/history.json @@ -94921,6 +94921,26 @@ ] } ] + }, + "5.2.0-rc.6": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "27042", + "title": "Chore: Bump fuselage to latest", + "userLogin": "gabriellsh", + "milestone": "5.2.0", + "contributors": [ + "gabriellsh" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index c53ca3665f0..8a07b288fe8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,21 @@ # 5.2.0 (Under Release Candidate Process) +## 5.2.0-rc.6 +`2022-10-10 · 1 🔠· 1 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` + +<details> +<summary>🔠Minor changes</summary> + + +- Chore: Bump fuselage to latest ([#27042](https://github.com/RocketChat/Rocket.Chat/pull/27042)) + +</details> + +### 👩â€ðŸ’»ðŸ‘¨â€ðŸ’» Core Team 🤓 + +- [@gabriellsh](https://github.com/gabriellsh) + ## 5.2.0-rc.5 `2022-10-10 · 3 🔠· 5 👩â€ðŸ’»ðŸ‘¨â€ðŸ’»` diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 4f5c635e123..deb5297f371 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.2.0-rc.5 +ENV RC_VERSION 5.2.0-rc.6 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index d81f85b3242..ef6b4416455 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "5.2.0-rc.5" + "version": "5.2.0-rc.6" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index e2af52ed98f..5d8a43422b1 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "5.2.0-rc.5", + "version": "5.2.0-rc.6", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index efc97571996..346c6650cf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "5.2.0-rc.5", + "version": "5.2.0-rc.6", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, -- GitLab From 53acec858e551d77acecf7036793b02831b5a268 Mon Sep 17 00:00:00 2001 From: Douglas Gubert <douglas.gubert@gmail.com> Date: Thu, 13 Oct 2022 14:18:26 -0300 Subject: [PATCH 107/107] Chore: Bump Apps-Engine version (#27059) --- apps/meteor/package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5d8a43422b1..64b97e8588a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -199,7 +199,7 @@ "@nivo/pie": "0.79.1", "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", - "@rocket.chat/apps-engine": "1.35.0-alpha.46", + "@rocket.chat/apps-engine": "latest", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "0.31.19", diff --git a/yarn.lock b/yarn.lock index 3158cedad7b..97291b27ac9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5021,9 +5021,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.35.0-alpha.46": - version: 1.35.0-alpha.46 - resolution: "@rocket.chat/apps-engine@npm:1.35.0-alpha.46" +"@rocket.chat/apps-engine@npm:^1.32.0": + version: 1.33.0 + resolution: "@rocket.chat/apps-engine@npm:1.33.0" dependencies: adm-zip: ^0.5.9 cryptiles: ^4.1.3 @@ -5031,14 +5031,13 @@ __metadata: semver: ^5.7.1 stack-trace: 0.0.10 uuid: ^3.4.0 - vm2: ^3.9.11 - checksum: 82376bf2765f2a7b548f9a30fcd4120c22e41b0605b3b74cf8a9cb685e08b8d55e1490cf736a45a6d780957e58a24e90234408124fef5badcadf9989b9a1c306 + checksum: d6182c86d88b2b06c2246937c86488663318966f653e17e089dbdd8e46799dec699a85f675dc0c2ec9c2ad9f195a4308443e8abbd2024f1f5ee9fa1b0a193f5d languageName: node linkType: hard -"@rocket.chat/apps-engine@npm:^1.32.0": - version: 1.33.0 - resolution: "@rocket.chat/apps-engine@npm:1.33.0" +"@rocket.chat/apps-engine@npm:latest": + version: 1.35.0 + resolution: "@rocket.chat/apps-engine@npm:1.35.0" dependencies: adm-zip: ^0.5.9 cryptiles: ^4.1.3 @@ -5046,7 +5045,8 @@ __metadata: semver: ^5.7.1 stack-trace: 0.0.10 uuid: ^3.4.0 - checksum: d6182c86d88b2b06c2246937c86488663318966f653e17e089dbdd8e46799dec699a85f675dc0c2ec9c2ad9f195a4308443e8abbd2024f1f5ee9fa1b0a193f5d + vm2: ^3.9.11 + checksum: 36c1aeeca2163a0bcde0de8f28eceedccb2cb0a55f014487c4924b72ba00e0969d9a28797a70aa40f8a6e3856b541608a0af251292b077e173c9e4fd9d0010f1 languageName: node linkType: hard @@ -5640,7 +5640,7 @@ __metadata: "@playwright/test": ^1.22.2 "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" - "@rocket.chat/apps-engine": 1.35.0-alpha.46 + "@rocket.chat/apps-engine": latest "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": 0.31.19 @@ -22995,7 +22995,7 @@ __metadata: resolution: "lamejs@https://github.com/zhuker/lamejs.git#commit=582bbba6a12f981b984d8fb9e1874499fed85675" dependencies: use-strict: 1.0.1 - checksum: fa829e0c170a65573e653b4d908a44aaf06a50e1bbade3b1217a300a03ccd59a537e294e2d924a584f9d70c7726a12d4c3af9c675436d48d08be5fb94b5eb400 + checksum: ed7f6f1c9629b53c17023eb04b4fc5a222e9c34fcb4a2f61214488fc64e5cfea825e4588d959c5fb20f3a91f0120103fa60307dd43df995d498ff5ddb6200cd9 languageName: node linkType: hard @@ -23117,7 +23117,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d + checksum: c9646e8377f8ce59648891ecdb7ebf1aea23349d145cc2f7d192785b8ede1677eb2582aa991175dcf18a63a092f81a1d3f8554a2826493f94d6db82b6007f409 languageName: node linkType: hard -- GitLab