From 8ade2d917f8368f2572ec399b7bd7d3367126cea Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 4 Jul 2024 15:12:52 -0300 Subject: [PATCH 01/32] feat: add new meeting colors and slots --- __mocks__/participants.mock.ts | 10 +- src/common/types/meeting-colors.types.ts | 120 ++++++++++++------ src/components/video/index.test.ts | 69 +++++----- src/components/video/index.ts | 10 +- src/components/who-is-online/index.test.ts | 23 ++-- src/components/who-is-online/index.ts | 12 +- src/components/who-is-online/types.ts | 2 +- src/services/slot/index.test.ts | 2 +- src/services/slot/index.ts | 20 ++- .../who-is-online/components/dropdown.ts | 5 +- .../who-is-online/who-is-online.ts | 8 +- 11 files changed, 158 insertions(+), 123 deletions(-) diff --git a/__mocks__/participants.mock.ts b/__mocks__/participants.mock.ts index a4acdbfb..7830b0ca 100644 --- a/__mocks__/participants.mock.ts +++ b/__mocks__/participants.mock.ts @@ -1,5 +1,5 @@ import { Avatar, Group, Participant } from '../src'; -import { MeetingColors, MeetingColorsHex } from '../src/common/types/meeting-colors.types'; +import { MEETING_COLORS } from '../src/common/types/meeting-colors.types'; import { ParticipantByGroupApi } from '../src/common/types/participant.types'; export const MOCK_AVATAR: Avatar = { @@ -16,9 +16,9 @@ export const MOCK_LOCAL_PARTICIPANT: Participant = { model3DUrl: 'unit-test-avatar-model.glb', }, slot: { - color: MeetingColorsHex[0], + color: MEETING_COLORS.turquoise, index: 0, - colorName: MeetingColors[0], + colorName: 'turquoise', textColor: '#000', timestamp: 0, }, @@ -36,7 +36,7 @@ export const MOCK_ABLY_PARTICIPANT_DATA_1 = { avatar: MOCK_AVATAR, participantId: MOCK_LOCAL_PARTICIPANT.id, slotIndex: 0, - color: MeetingColorsHex[0], + color: MEETING_COLORS.turquoise, }; export const MOCK_ABLY_PARTICIPANT_DATA_2 = { @@ -46,7 +46,7 @@ export const MOCK_ABLY_PARTICIPANT_DATA_2 = { avatar: MOCK_AVATAR, participantId: MOCK_LOCAL_PARTICIPANT.id, slotIndex: 1, - color: MeetingColorsHex[1], + color: MEETING_COLORS.orange, }; export const MOCK_PARTICIPANT_LIST: ParticipantByGroupApi[] = [ diff --git a/src/common/types/meeting-colors.types.ts b/src/common/types/meeting-colors.types.ts index 63a06d10..a2262ed3 100644 --- a/src/common/types/meeting-colors.types.ts +++ b/src/common/types/meeting-colors.types.ts @@ -1,41 +1,87 @@ -export enum MeetingColors { - 'turquoise', - 'orange', - 'blue', - 'pink', - 'purple', - 'green', +export const NAME_IS_WHITE_TEXT = [ + 'rosybrown', 'red', - 'bluedark', - 'pinklight', - 'purplelight', - 'greenlight', - 'orangelight', - 'bluelight', - 'redlight', + 'saddlebrown', + 'coral', + 'orange', 'brown', - 'yellow', - 'gray', -} + 'goldenrod', + 'olivegreen', + 'darkolivegreen', + 'seagreen', + 'lightsea', + 'teal', + 'cadetblue', + 'slategray', + 'mediumslateblue', + 'bluedark', + 'navy', + 'rebeccapurple', + 'purple', + 'vividorchid', + 'darkmagenta', + 'deepmagenta', + 'fuchsia', + 'violetred', + 'pink', + 'vibrantpink', + 'paleredviolet', + 'carmine', +]; -export const INDEX_IS_WHITE_TEXT = [1, 3, 4, 6, 7, 14, 16]; +export const MEETING_COLORS = { + turquoise: '#31E0B0', + orange: '#FF5E10', + blue: '#00ABF7', + pink: '#FF00BB', + purple: '#9C29FF', + green: '#6FDD00', + red: '#E30000', + bluedark: '#304AFF', + pinklight: '#FF89C4', + purplelight: '#D597FF', + greenlight: '#C6EC5C', + orangelight: '#FFA115', + bluelight: '#75DEFE', + redlight: '#FAA291', + brown: '#BB813F', + yellow: '#FFEF33', + olivegreen: '#93A000', + lightyellow: '#FAE391', + violetred: '#C03FA3', + rosybrown: '#B58787', + cadetblue: '#2095BB', + lightsteelblue: '#ABB5FF', + seagreen: '#04B45F', + palegreen: '#8DE990', + saddlebrown: '#964C42', + mediumgray: '#AFAFAF', + palesilver: '#D2BABA', + coral: '#DF6B6B', + bisque: '#FFD9C4', + goldenrod: '#DAA520', + tan: '#D2BD93', + darkolivegreen: '#536C27', + mint: '#ADE6DF', + lightsea: '#45AFAA', + teal: '#036E6E', + slategray: '#708090', + cyan: '#00FFFF', + mediumslateblue: '#6674D7', + navy: '#0013BB', + rebeccapurple: '#663399', + vividorchid: '#D429FF', + darkmagenta: '#810E81', + deepmagenta: '#C303C6', + Fuchsia: '#FA00FF', + lavendermagenta: '#EE82EE', + Thistle: '#EEB4DD', + vibrantpink: '#FF007A', + cottoncandy: '#FFC0DE', + paleredviolet: '#D96598', + carmine: '#B50A52', + gray: '#878291', +}; -export enum MeetingColorsHex { - '#31E0B0', - '#FF5E10', - '#00ABF7', - '#FF00BB', - '#9C29FF', - '#6FDD00', - '#E30000', - '#304AFF', - '#FF89C4', - '#D597FF', - '#C6EC5C', - '#FFA115', - '#75DEFE', - '#FAA291', - '#BB813F', - '#FFEF33', - '#878291', -} +export const MEETING_COLORS_ARRAY = Object.values(MEETING_COLORS); +export const MEETING_COLORS_KEYS = Object.keys(MEETING_COLORS); diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index cb174d09..ff2696ba 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -16,16 +16,15 @@ import { RealtimeEvent, TranscriptState, } from '../../common/types/events.types'; -import { MeetingColors, MeetingColorsHex } from '../../common/types/meeting-colors.types'; import { Participant, ParticipantType } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { useStore } from '../../common/utils/use-store'; import { IOC } from '../../services/io'; import { Presence3DManager } from '../../services/presence-3d-manager'; import { VideoFrameState } from '../../services/video-conference-manager/types'; -import { ComponentNames } from '../types'; import { VideoConference } from '.'; +import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; Object.assign(global, { TextDecoder, TextEncoder }); @@ -144,10 +143,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -191,10 +190,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -212,10 +211,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -248,10 +247,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -308,10 +307,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -589,12 +588,12 @@ describe('VideoConference', () => { const participantInfoList: Participant[] = [ { id: participants.value[MOCK_LOCAL_PARTICIPANT.id].id, - color: participants.value[MOCK_LOCAL_PARTICIPANT.id].slot?.colorName || 'gray', avatar: participants.value[MOCK_LOCAL_PARTICIPANT.id].avatar, name: participants.value[MOCK_LOCAL_PARTICIPANT.id].name, type: participants.value[MOCK_LOCAL_PARTICIPANT.id].type, isHost: participants.value[MOCK_LOCAL_PARTICIPANT.id].isHost ?? false, slot: participants.value[MOCK_LOCAL_PARTICIPANT.id].slot, + timestamp: participants.value[MOCK_LOCAL_PARTICIPANT.id].timestamp, }, ]; @@ -627,10 +626,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.GUEST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -679,12 +678,12 @@ describe('VideoConference', () => { isHost: true, avatar: MOCK_AVATAR, type: ParticipantType.HOST, - color: MeetingColors[0], + color: 'turquoise', slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -698,13 +697,13 @@ describe('VideoConference', () => { isHost: true, avatar: MOCK_AVATAR, type: ParticipantType.HOST, - color: MeetingColors[0], + color: 'turquoise', participantId: MOCK_LOCAL_PARTICIPANT.id, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }; @@ -730,10 +729,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, @@ -762,10 +761,10 @@ describe('VideoConference', () => { avatar: MOCK_AVATAR, type: ParticipantType.HOST, slot: { - colorName: MeetingColors[0], + colorName: 'turquoise', index: 0, - color: MeetingColorsHex[0], - textColor: MeetingColors[0], + color: MEETING_COLORS.turquoise, + textColor: '#fff', timestamp: 0, }, }, diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 75e81f46..16b1bb86 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -12,7 +12,6 @@ import { RealtimeEvent, TranscriptState, } from '../../common/types/events.types'; -import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; import { Participant, ParticipantType } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; @@ -34,6 +33,7 @@ import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; import { ParticipandToFrame, VideoComponentOptions } from './types'; +import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; const KICK_PARTICIPANTS_TIME = 1000 * 60; let KICK_PARTICIPANTS_TIMEOUT: ReturnType | null = null; @@ -324,7 +324,7 @@ export class VideoConference extends BaseComponent { ): Participant => { return { id: participant.id, - color: participant.data.slot?.color || MeetingColorsHex[16], + color: participant.data.slot?.color || MEETING_COLORS.gray, avatar: participant.data.avatar, type: participant.data.type, name: participant.data.name, @@ -549,12 +549,12 @@ export class VideoConference extends BaseComponent { const list: Participant[] = Object.values(participants).map((participant) => { return { id: participant.id, - color: participant.slot?.colorName || 'gray', + slot: participant.slot, avatar: participant.avatar, name: participant.name, type: participant.type, isHost: participant.isHost ?? false, - slot: participant.slot, + timestamp: participant.timestamp, }; }); @@ -710,7 +710,7 @@ export class VideoConference extends BaseComponent { const newHost = participant ? { id: participant.id, - color: participant.slot?.color || MeetingColorsHex[16], + color: participant.slot?.color || MEETING_COLORS.gray, avatar: participant.avatar, type: participant.type, name: participant.name, diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index 225f01bb..3d5ad70d 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -6,7 +6,6 @@ import { MOCK_ABLY_PARTICIPANT_DATA_1, } from '../../../__mocks__/participants.mock'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; -import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; import { StoreType } from '../../common/types/stores.types'; import { useStore } from '../../common/utils/use-store'; import { IOC } from '../../services/io'; @@ -17,6 +16,7 @@ import { ComponentNames } from '../types'; import { Avatar, WhoIsOnlineParticipant, TooltipData } from './types'; import { WhoIsOnline } from './index'; +import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; const generateMockParticipant = ({ id, @@ -68,8 +68,7 @@ describe('Who Is Online', () => { whoIsOnlineComponent['localParticipantId'] = MOCK_LOCAL_PARTICIPANT.id; - const gray = MeetingColorsHex[16]; - whoIsOnlineComponent['color'] = gray; + whoIsOnlineComponent['color'] = MEETING_COLORS.gray; }); afterEach(() => { @@ -813,7 +812,7 @@ describe('Who Is Online', () => { imageUrl: 'https://example.com/avatar.jpg', color: 'white', firstLetter: 'L', - slotIndex: 0, + letterColor: 'black', }; test('should return avatar data with image URL', () => { @@ -821,14 +820,14 @@ describe('Who Is Online', () => { avatar: mockAvatar as any, name: 'John Doe', color: '#007bff', - slotIndex: 1, + letterColor: 'black', }); expect(result).toEqual({ imageUrl: 'https://example.com/avatar.jpg', firstLetter: 'J', color: '#007bff', - slotIndex: 1, + letterColor: 'black', }); }); @@ -840,14 +839,14 @@ describe('Who Is Online', () => { }, name: 'Alice Smith', color: '#dc3545', - slotIndex: 2, + letterColor: 'black', }); expect(result).toEqual({ imageUrl: '', firstLetter: 'A', color: '#dc3545', - slotIndex: 2, + letterColor: 'black', }); }); @@ -856,14 +855,14 @@ describe('Who Is Online', () => { avatar: mockAvatar as any, name: 'User name', color: '#28a745', - slotIndex: 3, + letterColor: 'black', }); expect(result).toEqual({ imageUrl: 'https://example.com/avatar.jpg', firstLetter: 'U', color: '#28a745', - slotIndex: 3, + letterColor: 'black', }); }); @@ -875,14 +874,14 @@ describe('Who Is Online', () => { }, name: '', color: '#ffc107', - slotIndex: 4, + letterColor: 'black', }); expect(result).toEqual({ imageUrl: '', firstLetter: 'A', color: '#ffc107', - slotIndex: 4, + letterColor: 'black', }); }); }); diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index f3c03689..0e495e52 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -531,11 +531,11 @@ export class WhoIsOnline extends BaseComponent { activeComponents, id, name, - slot: { index, color }, + slot: { color, textColor }, } = participant; const disableDropdown = this.shouldDisableDropdown({ activeComponents, participantId: id }); - const avatar = this.getAvatar({ avatar: avatarLinks, color, name, slotIndex: index }); + const avatar = this.getAvatar({ avatar: avatarLinks, color, name, letterColor: textColor }); return { id, name, @@ -613,24 +613,24 @@ export class WhoIsOnline extends BaseComponent { /** * @function getAvatar * @description Processes the info of the participant's avatar - * @param { avatar: Avatar; name: string; color: string; slotIndex: number } data Information about the participant that will take part in their avatar somehow + * @param { avatar: Avatar; name: string; color: string; letterColor: string } data Information about the participant that will take part in their avatar somehow * @returns {Avatar} Information used to decide how to construct the participant's avatar html */ private getAvatar({ avatar, color, name, - slotIndex, + letterColor, }: { avatar: Avatar; name: string; color: string; - slotIndex: number; + letterColor: string; }) { const imageUrl = avatar?.imageUrl; const firstLetter = name?.at(0)?.toUpperCase() ?? 'A'; - return { imageUrl, firstLetter, color, slotIndex }; + return { imageUrl, firstLetter, color, letterColor }; } /** diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 7155c3b0..74e574d0 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -15,8 +15,8 @@ export interface TooltipData { export interface Avatar { imageUrl: string; firstLetter: string; - slotIndex: number; color: string; + letterColor: string; } export interface WhoIsOnlineParticipant { diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 240689af..183ef837 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -40,7 +40,7 @@ describe('slot service', () => { presence: { on: jest.fn(), get: jest.fn((callback) => { - callback(new Array(17).fill({})); + callback(new Array(50).fill({})); }), update: jest.fn(), }, diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index b034452a..9926352a 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -1,10 +1,6 @@ import * as Socket from '@superviz/socket-client'; -import { - INDEX_IS_WHITE_TEXT, - MeetingColors, - MeetingColorsHex, -} from '../../common/types/meeting-colors.types'; +import { NAME_IS_WHITE_TEXT, MEETING_COLORS } from '../../common/types/meeting-colors.types'; import { Participant, Slot } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { useStore } from '../../common/utils/use-store'; @@ -26,8 +22,8 @@ export class SlotService { * @returns void */ public async assignSlot(): Promise { - let slots = Array.from({ length: 16 }, (_, i) => i); - let slot = Math.floor(Math.random() * 16); + let slots = Array.from({ length: 50 }, (_, i) => i); + let slot = Math.floor(Math.random() * 50); const { localParticipant, participants } = useStore(StoreType.GLOBAL); try { @@ -35,7 +31,7 @@ export class SlotService { this.room.presence.get((presences) => { if (!presences || !presences.length) resolve(true); - if (presences.length >= 17) { + if (presences.length >= 50) { slots = []; reject(new Error('[SuperViz] - No more slots available')); return; @@ -56,11 +52,13 @@ export class SlotService { slot = slots.shift(); } + const color = Object.keys(MEETING_COLORS)[slot]; + const slotData = { index: slot, - color: MeetingColorsHex[slot], - textColor: INDEX_IS_WHITE_TEXT.includes(slot) ? '#fff' : '#000', - colorName: MeetingColors[slot], + color: MEETING_COLORS[color], + textColor: NAME_IS_WHITE_TEXT.includes(color) ? '#fff' : '#000', + colorName: color, timestamp: Date.now(), }; diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 345025d9..41c31251 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -4,7 +4,6 @@ import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; -import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; import { StoreType } from '../../../common/types/stores.types'; import { Avatar, Participant } from '../../../components/who-is-online/types'; import { WebComponentsBase } from '../../base'; @@ -115,7 +114,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { }; }; - private getAvatar({ color, imageUrl, firstLetter, slotIndex }: Avatar) { + private getAvatar({ color, imageUrl, firstLetter, letterColor }: Avatar) { if (imageUrl) { return html` `; } - const letterColor = INDEX_IS_WHITE_TEXT.includes(slotIndex) ? '#FFFFFF' : '#26242A'; - return html`
`; } - const letterColor = INDEX_IS_WHITE_TEXT.includes(slotIndex) ? '#FFFFFF' : '#26242A'; - return html`
id === participantId); this.everyoneFollowsMe = true; @@ -315,7 +312,6 @@ export class WhoIsOnline extends WebComponentsBaseElement { id, name, color, - slotIndex, }); } From 27104a454ea66d623f1263689cbcc06957b808c7 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 4 Jul 2024 15:24:20 -0300 Subject: [PATCH 02/32] test: wio tests --- .../who-is-online/components/dropdown.test.ts | 63 ++++++------------- .../who-is-online/who-is-online.test.ts | 39 +++--------- 2 files changed, 25 insertions(+), 77 deletions(-) diff --git a/src/web-components/who-is-online/components/dropdown.test.ts b/src/web-components/who-is-online/components/dropdown.test.ts index 582268e2..27947374 100644 --- a/src/web-components/who-is-online/components/dropdown.test.ts +++ b/src/web-components/who-is-online/components/dropdown.test.ts @@ -1,9 +1,13 @@ import '.'; -import { MeetingColorsHex } from '../../../common/types/meeting-colors.types'; +import { MEETING_COLORS } from '../../../common/types/meeting-colors.types'; + import { StoreType } from '../../../common/types/stores.types'; import sleep from '../../../common/utils/sleep'; import { useStore } from '../../../common/utils/use-store'; -import { Participant, WIODropdownOptions } from '../../../components/who-is-online/types'; +import { + WhoIsOnlineParticipant, + WIODropdownOptions, +} from '../../../components/who-is-online/types'; interface elementProps { position: string; @@ -14,14 +18,14 @@ interface elementProps { icons?: string[]; } -const MOCK_PARTICIPANTS: Participant[] = [ +const MOCK_PARTICIPANTS: WhoIsOnlineParticipant[] = [ { name: 'John Zero', avatar: { imageUrl: 'https://example.com', - color: MeetingColorsHex[0], + color: MEETING_COLORS.turquoise, firstLetter: 'J', - slotIndex: 0, + letterColor: '#fff', }, id: '1', activeComponents: ['whoisonline', 'presence'], @@ -40,14 +44,15 @@ const MOCK_PARTICIPANTS: Participant[] = [ label: WIODropdownOptions.PRIVATE, }, ], + isPrivate: false, }, { name: 'John Uno', avatar: { imageUrl: '', - color: MeetingColorsHex[1], + color: MEETING_COLORS.orange, firstLetter: 'J', - slotIndex: 1, + letterColor: '#fff', }, id: '2', activeComponents: ['whoisonline'], @@ -63,14 +68,15 @@ const MOCK_PARTICIPANTS: Participant[] = [ label: WIODropdownOptions.LOCAL_FOLLOW, }, ], + isPrivate: false, }, { name: 'John Doe', avatar: { imageUrl: '', - color: MeetingColorsHex[2], + color: MEETING_COLORS.brown, firstLetter: 'J', - slotIndex: 2, + letterColor: '#26242A', }, id: '3', activeComponents: ['whoisonline', 'presence'], @@ -86,6 +92,7 @@ const MOCK_PARTICIPANTS: Participant[] = [ label: WIODropdownOptions.LOCAL_FOLLOW, }, ], + isPrivate: false, }, ]; @@ -236,41 +243,6 @@ describe('who-is-online-dropdown', () => { expect(spy).toHaveBeenCalled(); }); - test('should give a black color to the letter when the slotIndex is not in the textColorValues', async () => { - createEl({ position: 'bottom' }); - - const { extras } = useStore(StoreType.WHO_IS_ONLINE); - extras.publish([MOCK_PARTICIPANTS[2]]); - await sleep(); - - const letter = element()?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - - const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[2].avatar.slotIndex as number]; - expect(letter?.getAttribute('style')).toBe( - `background-color: ${backgroundColor}; color: #26242A`, - ); - }); - - test('should give a white color to the letter when the slotIndex is in the textColorValues', async () => { - const participant = { - ...MOCK_PARTICIPANTS[0], - slotIndex: 1, - color: MeetingColorsHex[1], - }; - - createEl({ position: 'bottom' }); - const { extras } = useStore(StoreType.WHO_IS_ONLINE); - extras.publish([MOCK_PARTICIPANTS[1]]); - await sleep(); - - const letter = element()?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - - const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[1].avatar.slotIndex as number]; - expect(letter?.getAttribute('style')).toBe( - `background-color: ${backgroundColor}; color: #FFFFFF`, - ); - }); - test('should not render participants when there is no participant', async () => { createEl({ position: 'bottom' }); const { extras } = useStore(StoreType.WHO_IS_ONLINE); @@ -301,8 +273,9 @@ describe('who-is-online-dropdown', () => { imageUrl: '', color: 'red', firstLetter: 'J', - slotIndex: 0, + letterColor: '#fff', }, + isPrivate: false, id: '1', name: 'John Zero', activeComponents: ['whoisonline', 'presence'], diff --git a/src/web-components/who-is-online/who-is-online.test.ts b/src/web-components/who-is-online/who-is-online.test.ts index e7e119a7..1f7f4811 100644 --- a/src/web-components/who-is-online/who-is-online.test.ts +++ b/src/web-components/who-is-online/who-is-online.test.ts @@ -2,7 +2,7 @@ import '.'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { RealtimeEvent } from '../../common/types/events.types'; -import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; +import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; import { StoreType } from '../../common/types/stores.types'; import sleep from '../../common/utils/sleep'; import { useStore } from '../../common/utils/use-store'; @@ -17,9 +17,9 @@ const MOCK_PARTICIPANTS: WhoIsOnlineParticipant[] = [ name: 'John Zero', avatar: { imageUrl: 'https://example.com', - color: MeetingColorsHex[0], + color: MEETING_COLORS.turquoise, firstLetter: 'J', - slotIndex: 0, + letterColor: '#fff', }, id: '1', activeComponents: ['whoisonline', 'presence'], @@ -44,9 +44,9 @@ const MOCK_PARTICIPANTS: WhoIsOnlineParticipant[] = [ name: 'John Uno', avatar: { imageUrl: '', - color: MeetingColorsHex[1], + color: MEETING_COLORS.orange, firstLetter: 'J', - slotIndex: 1, + letterColor: '#fff', }, id: '2', activeComponents: ['whoisonline'], @@ -68,9 +68,9 @@ const MOCK_PARTICIPANTS: WhoIsOnlineParticipant[] = [ name: 'John Doe', avatar: { imageUrl: '', - color: MeetingColorsHex[2], + color: MEETING_COLORS.brown, firstLetter: 'J', - slotIndex: 2, + letterColor: '#26242A', }, id: '3', activeComponents: ['whoisonline', 'presence'], @@ -187,31 +187,6 @@ describe('Who Is Online', () => { expect(extraParticipants).toBeTruthy(); }); - test('should give a black color to the letter when the slotIndex is not in the textColorValues', async () => { - const { participants } = useStore(StoreType.WHO_IS_ONLINE); - participants.publish([MOCK_PARTICIPANTS[2]]); - await sleep(); - - const letter = element?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[2].avatar.slotIndex as number]; - expect(letter?.getAttribute('style')).toBe( - `background-color: ${backgroundColor}; color: #26242A`, - ); - }); - - test('should give a white color to the letter when the slotIndex is in the textColorValues', async () => { - const { participants } = useStore(StoreType.WHO_IS_ONLINE); - participants.publish([MOCK_PARTICIPANTS[1]]); - await sleep(); - - const letter = element?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - - const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[1].avatar.slotIndex as number]; - expect(letter?.getAttribute('style')).toBe( - `background-color: ${backgroundColor}; color: #FFFFFF`, - ); - }); - test('should toggle open property', () => { expect(element['open']).toBeFalsy(); From 975c475a6110df5ab3882f766a2976963b53c648 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 4 Jul 2024 17:45:41 -0300 Subject: [PATCH 03/32] feat: fetch pointer image by color name --- src/common/types/meeting-colors.types.ts | 7 ++++--- src/components/presence-mouse/canvas/index.ts | 2 +- src/components/presence-mouse/html/index.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common/types/meeting-colors.types.ts b/src/common/types/meeting-colors.types.ts index a2262ed3..9ec204c8 100644 --- a/src/common/types/meeting-colors.types.ts +++ b/src/common/types/meeting-colors.types.ts @@ -12,7 +12,7 @@ export const NAME_IS_WHITE_TEXT = [ 'lightsea', 'teal', 'cadetblue', - 'slategray', + 'pastelblue', 'mediumslateblue', 'bluedark', 'navy', @@ -27,6 +27,7 @@ export const NAME_IS_WHITE_TEXT = [ 'vibrantpink', 'paleredviolet', 'carmine', + 'wine', ]; export const MEETING_COLORS = { @@ -55,7 +56,7 @@ export const MEETING_COLORS = { seagreen: '#04B45F', palegreen: '#8DE990', saddlebrown: '#964C42', - mediumgray: '#AFAFAF', + pastelblue: '#77A1CC', palesilver: '#D2BABA', coral: '#DF6B6B', bisque: '#FFD9C4', @@ -65,7 +66,7 @@ export const MEETING_COLORS = { mint: '#ADE6DF', lightsea: '#45AFAA', teal: '#036E6E', - slategray: '#708090', + wine: '#760040', cyan: '#00FFFF', mediumslateblue: '#6674D7', navy: '#0013BB', diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 7a5250b5..60a41d4b 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -330,7 +330,7 @@ export class PointersCanvas extends BaseComponent { const pointerUser = divPointer.getElementsByClassName('pointer-mouse')[0] as HTMLDivElement; if (pointerUser) { - pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/pointers-v2/${mouse.slot.index}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${mouse.slot.colorName}.svg)`; } if (mouseUser) { diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index e0929f7e..d087755b 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -691,7 +691,7 @@ export class PointersHTML extends BaseComponent { const pointerUser = mouseFollower.getElementsByClassName('pointer-mouse')[0] as HTMLDivElement; if (pointerUser) { - pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/pointers-v2/${participant.slot.index}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${participant.slot.colorName}.svg)`; } if (mouseUser) { From e4d39012e915f56e61850b6896a6617b0d472928 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 8 Jul 2024 17:58:09 -0300 Subject: [PATCH 04/32] fix: colors names --- src/common/types/meeting-colors.types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/types/meeting-colors.types.ts b/src/common/types/meeting-colors.types.ts index 9ec204c8..1de9b662 100644 --- a/src/common/types/meeting-colors.types.ts +++ b/src/common/types/meeting-colors.types.ts @@ -74,9 +74,9 @@ export const MEETING_COLORS = { vividorchid: '#D429FF', darkmagenta: '#810E81', deepmagenta: '#C303C6', - Fuchsia: '#FA00FF', + fuchsia: '#FA00FF', lavendermagenta: '#EE82EE', - Thistle: '#EEB4DD', + thistle: '#EEB4DD', vibrantpink: '#FF007A', cottoncandy: '#FFC0DE', paleredviolet: '#D96598', From 5f041cad38380b69c3732e441ffba215dd6fef5e Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 9 Jul 2024 12:30:33 -0300 Subject: [PATCH 05/32] refactor: improve local participant updates in the global state --- __mocks__/participants.mock.ts | 1 - src/common/types/participant.types.ts | 1 - src/components/presence-mouse/canvas/index.ts | 2 +- src/components/presence-mouse/html/index.ts | 2 +- src/components/video/index.test.ts | 9 +-- src/components/video/index.ts | 12 ++-- src/components/video/types.ts | 6 +- src/core/launcher/index.ts | 60 +++++++------------ .../who-is-online/components/messages.ts | 2 +- 9 files changed, 42 insertions(+), 53 deletions(-) diff --git a/__mocks__/participants.mock.ts b/__mocks__/participants.mock.ts index 7830b0ca..e9851742 100644 --- a/__mocks__/participants.mock.ts +++ b/__mocks__/participants.mock.ts @@ -10,7 +10,6 @@ export const MOCK_AVATAR: Avatar = { export const MOCK_LOCAL_PARTICIPANT: Participant = { id: 'unit-test-local-participant-id', name: 'unit-test-local-participant-name', - color: '#000', avatar: { imageUrl: 'unit-test-avatar-thumbnail.png', model3DUrl: 'unit-test-avatar-model.glb', diff --git a/src/common/types/participant.types.ts b/src/common/types/participant.types.ts index 26e5efcb..2677808e 100644 --- a/src/common/types/participant.types.ts +++ b/src/common/types/participant.types.ts @@ -18,7 +18,6 @@ export interface Participant { id: string; name?: string; type?: ParticipantType; - color?: string; slot?: Slot; avatar?: Avatar; isHost?: boolean; diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 60a41d4b..82d3640d 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -330,7 +330,7 @@ export class PointersCanvas extends BaseComponent { const pointerUser = divPointer.getElementsByClassName('pointer-mouse')[0] as HTMLDivElement; if (pointerUser) { - pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${mouse.slot.colorName}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${mouse.slot?.colorName}.svg)`; } if (mouseUser) { diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index d087755b..922e39a4 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -691,7 +691,7 @@ export class PointersHTML extends BaseComponent { const pointerUser = mouseFollower.getElementsByClassName('pointer-mouse')[0] as HTMLDivElement; if (pointerUser) { - pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${participant.slot.colorName}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/mouse-pointers/${participant.slot?.colorName}.svg)`; } if (mouseUser) { diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index ff2696ba..b41524c4 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -22,6 +22,7 @@ import { useStore } from '../../common/utils/use-store'; import { IOC } from '../../services/io'; import { Presence3DManager } from '../../services/presence-3d-manager'; import { VideoFrameState } from '../../services/video-conference-manager/types'; +import { ParticipantToFrame } from './types'; import { VideoConference } from '.'; import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; @@ -678,7 +679,6 @@ describe('VideoConference', () => { isHost: true, avatar: MOCK_AVATAR, type: ParticipantType.HOST, - color: 'turquoise', slot: { colorName: 'turquoise', index: 0, @@ -691,14 +691,15 @@ describe('VideoConference', () => { VideoConferenceInstance['onRealtimeParticipantsDidChange'](participant); - const expectedParticipants = { + const expectedParticipants: ParticipantToFrame = { timestamp: 0, - name: MOCK_LOCAL_PARTICIPANT.name, + name: MOCK_LOCAL_PARTICIPANT.name as string, isHost: true, avatar: MOCK_AVATAR, type: ParticipantType.HOST, - color: 'turquoise', participantId: MOCK_LOCAL_PARTICIPANT.id, + color: MEETING_COLORS.turquoise, + id: MOCK_LOCAL_PARTICIPANT.id, slot: { colorName: 'turquoise', index: 0, diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 16b1bb86..2df91aee 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -32,7 +32,7 @@ import { import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; -import { ParticipandToFrame, VideoComponentOptions } from './types'; +import { ParticipantToFrame, VideoComponentOptions } from './types'; import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; const KICK_PARTICIPANTS_TIME = 1000 * 60; @@ -321,14 +321,17 @@ export class VideoConference extends BaseComponent { * */ private createParticipantFromPresence = ( participant: PresenceEvent, - ): Participant => { + ): ParticipantToFrame => { return { + participantId: participant.id, id: participant.id, color: participant.data.slot?.color || MEETING_COLORS.gray, avatar: participant.data.avatar, type: participant.data.type, name: participant.data.name, isHost: participant.data.isHost, + timestamp: participant.timestamp, + slot: participant.data.slot, }; }; @@ -672,11 +675,12 @@ export class VideoConference extends BaseComponent { */ private onRealtimeParticipantsDidChange = (participants: Participant[]): void => { this.logger.log('video conference @ on participants did change', participants); - const participantList: ParticipandToFrame[] = participants.map((participant) => { + const participantList: ParticipantToFrame[] = participants.map((participant) => { return { + id: participant.id, timestamp: participant.timestamp, participantId: participant.id, - color: participant.slot?.colorName ?? 'gray', + color: participant.slot?.color || MEETING_COLORS.gray, name: participant.name, isHost: participant.isHost ?? false, avatar: participant.avatar, diff --git a/src/components/video/types.ts b/src/components/video/types.ts index cf5957fd..040407ab 100644 --- a/src/components/video/types.ts +++ b/src/components/video/types.ts @@ -1,4 +1,4 @@ -import { Avatar, ParticipantType } from '../../common/types/participant.types'; +import { Avatar, ParticipantType, Slot } from '../../common/types/participant.types'; import { DevicesOptions } from '../../common/types/sdk-options.types'; import { CamerasPosition, @@ -46,7 +46,8 @@ export interface VideoComponentOptions { }; } -export type ParticipandToFrame = { +export type ParticipantToFrame = { + id: string; timestamp: number; participantId: string; color: string; @@ -54,4 +55,5 @@ export type ParticipandToFrame = { isHost: boolean; avatar?: Avatar; type: ParticipantType; + slot: Slot; }; diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 76dc1820..9ce93caf 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -28,11 +28,11 @@ export class Launcher extends Observable implements DefaultLauncher { private activeComponents: ComponentNames[] = []; private componentsToAttachAfterJoin: Partial[] = []; private activeComponentsInstances: Partial[] = []; + private participant: Participant; private ioc: IOC; private room: Socket.Room; private eventBus: EventBus = new EventBus(); - private timestamp: number = 0; private useStore = useStore.bind(this) as typeof useStore; @@ -47,6 +47,9 @@ export class Launcher extends Observable implements DefaultLauncher { localParticipant.publish({ ...participant }); participants.subscribe(this.onParticipantListUpdate); isDomainWhitelisted.subscribe(this.onAuthentication); + localParticipant.subscribe((participant) => { + this.participant = participant; + }); group.publish(participantGroup); this.ioc = new IOC(localParticipant.value); @@ -144,7 +147,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.activeComponents.splice(this.activeComponents.indexOf(component.name), 1); localParticipant.publish({ - ...localParticipant.value, + ...this.participant, activeComponents: this.activeComponents, }); }; @@ -244,14 +247,9 @@ export class Launcher extends Observable implements DefaultLauncher { this.logger.log('launcher service @ onParticipantListUpdate', participants); const { localParticipant } = useStore(StoreType.GLOBAL); - const participant: Participant = Object.values(participants) - .filter((participant) => participant.id === localParticipant.value.id) - .map((participant) => { - return { - ...participant, - color: participant.slot?.color, - }; - })[0]; + const participant: Participant = Object.values(participants).find( + (participant) => participant.id === localParticipant.value.id, + ); if (!participant || isEqual(localParticipant.value, participant)) return; @@ -279,8 +277,7 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {void} */ private onParticipantJoined = (participant: Socket.PresenceEvent): void => { - const { localParticipant } = useStore(StoreType.GLOBAL); - if (participant.id !== localParticipant.value.id) return; + if (participant.id !== this.participant.id) return; this.logger.log('launcher service @ onParticipantJoined - local participant joined'); this.attachComponentsAfterJoin(); @@ -301,7 +298,7 @@ export class Launcher extends Observable implements DefaultLauncher { private startIOC = (): void => { this.logger.log('launcher service @ startIOC'); - const { participants, localParticipant } = useStore(StoreType.GLOBAL); + const { participants } = useStore(StoreType.GLOBAL); // retrieve the current participants in the room this.ioc.stateSubject.subscribe((state) => { @@ -321,9 +318,9 @@ export class Launcher extends Observable implements DefaultLauncher { }; }); - participantsMap[localParticipant.value.id] = { - ...participantsMap[localParticipant.value.id], - ...localParticipant.value, + participantsMap[this.participant.id] = { + ...participantsMap[this.participant.id], + ...this.participant, }; participants.publish(participantsMap); @@ -351,21 +348,18 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoinedIOC = async ( presence: Socket.PresenceEvent, ): Promise => { - const { localParticipant } = useStore(StoreType.GLOBAL); - if (presence.id !== localParticipant.value.id) return; + if (presence.id !== this.participant.id) return; // Assign a slot to the participant const slot = new SlotService(this.room); await slot.assignSlot(); - this.timestamp = presence.timestamp; - - this.room.presence.update(localParticipant.value); + this.room.presence.update(this.participant); this.logger.log('launcher service @ onParticipantJoined - local participant joined'); this.onParticipantJoined(presence); - this.publish(ParticipantEvent.LOCAL_JOINED, localParticipant.value); - this.publish(ParticipantEvent.JOINED, localParticipant.value); + this.publish(ParticipantEvent.LOCAL_JOINED, this.participant); + this.publish(ParticipantEvent.JOINED, this.participant); }; /** @@ -399,27 +393,17 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantUpdatedIOC = (presence: Socket.PresenceEvent): void => { const { localParticipant } = useStore(StoreType.GLOBAL); - if ( - localParticipant.value && - presence.id === localParticipant.value.id && - !isEqual(localParticipant.value, presence.data) - ) { - if (presence.data.timestamp === this.timestamp) { - this.timestamp = 0; - return; - } - + if (localParticipant.value && presence.id === localParticipant.value.id) { localParticipant.publish({ ...presence.data, ...localParticipant.value, timestamp: presence.timestamp, } as Participant); - this.timestamp = presence.timestamp; - this.room.presence.update(localParticipant.value); - - this.publish(ParticipantEvent.LOCAL_UPDATED, presence.data); - + this.publish(ParticipantEvent.LOCAL_UPDATED, { + ...presence.data, + ...localParticipant.value, + }); this.logger.log('Publishing ParticipantEvent.UPDATED', presence.data); } diff --git a/src/web-components/who-is-online/components/messages.ts b/src/web-components/who-is-online/components/messages.ts index f54c8d7a..f520ad5d 100644 --- a/src/web-components/who-is-online/components/messages.ts +++ b/src/web-components/who-is-online/components/messages.ts @@ -38,7 +38,7 @@ export class WhoIsOnlineMessages extends WebComponentsBaseElement { super(); const { localParticipant } = this.useStore(StoreType.GLOBAL); localParticipant.subscribe((participant: Participant) => { - this.participantColor = participant.color; + this.participantColor = participant.slot.color; }); const { following } = this.useStore(StoreType.WHO_IS_ONLINE); From dd1eb8c0e0b52b203cf0382bc30387e3a3af7936 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 9 Jul 2024 15:03:51 -0300 Subject: [PATCH 06/32] fix: authentication destroy errors --- src/core/launcher/index.test.ts | 2 +- src/core/launcher/index.ts | 8 +++++++- src/web-components/who-is-online/components/messages.ts | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index afcf3463..ed13e8be 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -249,7 +249,7 @@ describe('Launcher', () => { LauncherInstance['onAuthentication'](false); expect(LauncherInstance.destroy).toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( - `Room can't be initialized because this website's domain is not whitelisted. If you are the developer, please add your domain in https://dashboard.superviz.com/developer`, + `[SuperViz] Room cannot be initialized because this website's domain is not whitelisted. If you are the developer, please add your domain in https://dashboard.superviz.com/developer`, ); }); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 9ce93caf..46dad2dc 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -228,12 +228,18 @@ export class Launcher extends Observable implements DefaultLauncher { return true; }; + /** + * @function onAuthentication + * @description on authentication + * @param isAuthenticated - return if the user is authenticated + * @returns {void} + */ private onAuthentication = (isAuthenticated: boolean): void => { if (isAuthenticated) return; this.destroy(); console.error( - `Room can't be initialized because this website's domain is not whitelisted. If you are the developer, please add your domain in https://dashboard.superviz.com/developer`, + `[SuperViz] Room cannot be initialized because this website's domain is not whitelisted. If you are the developer, please add your domain in https://dashboard.superviz.com/developer`, ); }; diff --git a/src/web-components/who-is-online/components/messages.ts b/src/web-components/who-is-online/components/messages.ts index f520ad5d..5c3d9f71 100644 --- a/src/web-components/who-is-online/components/messages.ts +++ b/src/web-components/who-is-online/components/messages.ts @@ -10,6 +10,7 @@ import importStyle from '../../base/utils/importStyle'; import { messagesStyle } from '../css'; import { HorizontalSide, VerticalSide } from './types'; +import { MEETING_COLORS } from '../../../common/types/meeting-colors.types'; const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, messagesStyle]; @@ -38,7 +39,7 @@ export class WhoIsOnlineMessages extends WebComponentsBaseElement { super(); const { localParticipant } = this.useStore(StoreType.GLOBAL); localParticipant.subscribe((participant: Participant) => { - this.participantColor = participant.slot.color; + this.participantColor = participant.slot?.color ?? MEETING_COLORS.gray; }); const { following } = this.useStore(StoreType.WHO_IS_ONLINE); From 5793d081d9e0d87dcf231b0d258d250ec7969d3b Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 9 Jul 2024 16:50:20 -0300 Subject: [PATCH 07/32] feat: only assign slot to user when it's necessary --- src/components/presence-mouse/canvas/index.ts | 5 +- src/components/presence-mouse/html/index.ts | 5 +- src/core/launcher/index.ts | 20 ++-- src/services/slot/index.test.ts | 79 ++++++++++++-- src/services/slot/index.ts | 102 ++++++++++++++++-- 5 files changed, 184 insertions(+), 27 deletions(-) diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 82d3640d..d8f328c5 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -8,6 +8,7 @@ import { Logger } from '../../../common/utils'; import { BaseComponent } from '../../base'; import { ComponentNames } from '../../types'; import { Camera, ParticipantMouse, PresenceMouseProps, Transform } from '../types'; +import { MEETING_COLORS } from '../../../common/types/meeting-colors.types'; export class PointersCanvas extends BaseComponent { public name: ComponentNames; @@ -334,8 +335,8 @@ export class PointersCanvas extends BaseComponent { } if (mouseUser) { - mouseUser.style.color = mouse.slot.textColor; - mouseUser.style.backgroundColor = mouse.slot.color; + mouseUser.style.color = mouse.slot?.textColor ?? '#fff'; + mouseUser.style.backgroundColor = mouse.slot?.color ?? MEETING_COLORS.gray; mouseUser.innerHTML = mouse.name; } diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 922e39a4..4bffb602 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -16,6 +16,7 @@ import { Transform, VoidElements, } from '../types'; +import { MEETING_COLORS } from '../../../common/types/meeting-colors.types'; export class PointersHTML extends BaseComponent { public name: ComponentNames; @@ -695,8 +696,8 @@ export class PointersHTML extends BaseComponent { } if (mouseUser) { - mouseUser.style.color = participant.slot.textColor; - mouseUser.style.backgroundColor = participant.slot.color; + mouseUser.style.color = participant.slot?.textColor ?? MEETING_COLORS.gray; + mouseUser.style.backgroundColor = participant.slot?.color ?? '#fff'; mouseUser.innerHTML = participant.name; } diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 46dad2dc..f35dd8e7 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -2,7 +2,7 @@ import * as Socket from '@superviz/socket-client'; import { isEqual } from 'lodash'; import { ParticipantEvent } from '../../common/types/events.types'; -import { Participant } from '../../common/types/participant.types'; +import { Participant, ParticipantType } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { Observable } from '../../common/utils'; import { Logger } from '../../common/utils/logger'; @@ -33,6 +33,7 @@ export class Launcher extends Observable implements DefaultLauncher { private ioc: IOC; private room: Socket.Room; private eventBus: EventBus = new EventBus(); + private slotService: SlotService; private useStore = useStore.bind(this) as typeof useStore; @@ -47,9 +48,7 @@ export class Launcher extends Observable implements DefaultLauncher { localParticipant.publish({ ...participant }); participants.subscribe(this.onParticipantListUpdate); isDomainWhitelisted.subscribe(this.onAuthentication); - localParticipant.subscribe((participant) => { - this.participant = participant; - }); + localParticipant.subscribe(this.onParticipantLocalParticipantUpdateOnStore); group.publish(participantGroup); this.ioc = new IOC(localParticipant.value); @@ -243,6 +242,16 @@ export class Launcher extends Observable implements DefaultLauncher { ); }; + /** + * @function onParticipantLocalParticipantUpdateOnStore + * @description on participant local participant update on store + * @param {Participant} participant - new participant data + * @returns {void} + */ + private onParticipantLocalParticipantUpdateOnStore = (participant: Participant): void => { + this.participant = participant; + }; + /** * @function onParticipantListUpdate * @description on participant list update @@ -357,8 +366,7 @@ export class Launcher extends Observable implements DefaultLauncher { if (presence.id !== this.participant.id) return; // Assign a slot to the participant - const slot = new SlotService(this.room); - await slot.assignSlot(); + this.slotService = new SlotService(this.room, this.useStore); this.room.presence.update(this.participant); diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 183ef837..06357815 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -1,4 +1,10 @@ import { SlotService } from '.'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; +import { MEETING_COLORS, MEETING_COLORS_KEYS } from '../../common/types/meeting-colors.types'; +import { Participant } from '../../common/types/participant.types'; +import { StoreType } from '../../common/types/stores.types'; +import { useStore } from '../../common/utils/use-store'; +import { ComponentNames } from '../../components/types'; describe('slot service', () => { afterEach(() => { @@ -16,11 +22,7 @@ describe('slot service', () => { }, } as any; - const participant = { - id: '123', - } as any; - - const instance = new SlotService(room); + const instance = new SlotService(room, useStore); const result = await instance.assignSlot(); expect(instance['slotIndex']).toBeDefined(); @@ -33,6 +35,30 @@ describe('slot service', () => { }); }); + test('should remove the slot from the participant', async () => { + const room = { + presence: { + on: jest.fn(), + update: jest.fn(), + }, + } as any; + + const instance = new SlotService(room, useStore); + instance['slotIndex'] = 0; + instance.setDefaultSlot(); + + expect(instance['slotIndex']).toBeNull(); + expect(room.presence.update).toHaveBeenCalledWith({ + slot: { + index: null, + color: expect.any(String), + textColor: expect.any(String), + colorName: expect.any(String), + timestamp: expect.any(Number), + }, + }); + }); + test('if there are no more slots available, it should throw an error', async () => { console.error = jest.fn(); @@ -46,10 +72,10 @@ describe('slot service', () => { }, } as any; - const instance = new SlotService(room); + const instance = new SlotService(room, useStore); await instance.assignSlot(); - expect(instance['slotIndex']).toBeUndefined(); + expect(instance['slotIndex']).toBeNull(); }); test('if the slot is already in use, it should assign a new slot', async () => { @@ -79,7 +105,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room); + const instance = new SlotService(room, useStore); const result = await instance.assignSlot(); expect(instance['slotIndex']).toBeDefined(); @@ -91,4 +117,41 @@ describe('slot service', () => { timestamp: expect.any(Number), }); }); + + test("should remove the slot from the participant when the participant don't need it anymore", async () => { + const room = { + presence: { + on: jest.fn(), + update: jest.fn(), + }, + } as any; + + const instance = new SlotService(room, useStore); + instance['slotIndex'] = 0; + + const event: Participant = { + ...MOCK_LOCAL_PARTICIPANT, + slot: { + index: 0, + color: MEETING_COLORS.turquoise, + colorName: MEETING_COLORS_KEYS[0], + textColor: '#000', + timestamp: 0, + }, + activeComponents: [], + }; + + const { localParticipant } = useStore(StoreType.GLOBAL); + localParticipant.publish(event); + + expect(room.presence.update).toHaveBeenCalledWith({ + slot: { + index: null, + color: expect.any(String), + textColor: expect.any(String), + colorName: expect.any(String), + timestamp: expect.any(Number), + }, + }); + }); }); diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 9926352a..cde157dd 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -1,19 +1,33 @@ import * as Socket from '@superviz/socket-client'; -import { NAME_IS_WHITE_TEXT, MEETING_COLORS } from '../../common/types/meeting-colors.types'; -import { Participant, Slot } from '../../common/types/participant.types'; -import { StoreType } from '../../common/types/stores.types'; +import { + NAME_IS_WHITE_TEXT, + MEETING_COLORS, + MEETING_COLORS_KEYS, +} from '../../common/types/meeting-colors.types'; +import { Participant, ParticipantType, Slot } from '../../common/types/participant.types'; +import { Store, StoreType } from '../../common/types/stores.types'; import { useStore } from '../../common/utils/use-store'; +import { ComponentNames } from '../../components/types'; export class SlotService { - private room: Socket.Room; + private slotIndex: number | null = null; - private slotIndex: number; - - constructor(room: Socket.Room) { + constructor( + private room: Socket.Room, + private useStore: (name: T) => Store, + ) { this.room = room; this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); + + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe(this.onParticipantLocalParticipantUpdateOnStore); + + /** + * When the participant enters the room, is setted the default slot + */ + this.setDefaultSlot(); } /** @@ -48,6 +62,7 @@ export class SlotService { }); const isUsing = !slots.includes(slot); + if (isUsing) { slot = slots.shift(); } @@ -86,10 +101,44 @@ export class SlotService { } } - private onPresenceUpdate = async (event: Socket.PresenceEvent) => { + /** + * @function setDefaultSlot + * @description Removes the slot from the participant + * @returns void + */ + public setDefaultSlot() { const { localParticipant, participants } = useStore(StoreType.GLOBAL); - if (!event.data.slot || !localParticipant.value?.slot) return; + const slot: Slot = { + index: null, + color: MEETING_COLORS.gray, + textColor: '#fff', + colorName: 'gray', + timestamp: Date.now(), + }; + + this.slotIndex = slot.index; + + localParticipant.publish({ + ...localParticipant.value, + slot: slot, + }); + + participants.publish({ + ...participants.value, + [localParticipant.value.id]: { + ...participants.value[localParticipant.value.id], + slot, + }, + }); + + this.room.presence.update({ slot }); + } + + private onPresenceUpdate = async (event: Socket.PresenceEvent) => { + const { localParticipant } = this.useStore(StoreType.GLOBAL); + + if (!event.data.slot || !localParticipant.value?.slot?.index) return; if (event.id === localParticipant.value.id) { localParticipant.publish({ @@ -114,4 +163,39 @@ export class SlotService { ); } }; + + /** + * @function onParticipantLocalParticipantUpdateOnStore + * @description on participant local participant update on store + * @param {Participant} participant - new participant data + * @returns {void} + */ + private onParticipantLocalParticipantUpdateOnStore = (participant: Participant): void => { + const COMPONENTS_THAT_NEED_SLOT = [ + ComponentNames.FORM_ELEMENTS, + ComponentNames.WHO_IS_ONLINE, + ComponentNames.PRESENCE, + ComponentNames.PRESENCE_AUTODESK, + ComponentNames.PRESENCE_MATTERPORT, + ComponentNames.PRESENCE_THREEJS, + ]; + + const componentsNeedSlot = COMPONENTS_THAT_NEED_SLOT.some((component) => { + return participant.activeComponents.includes(component); + }); + + const videoNeedSlot = + participant.activeComponents.includes(ComponentNames.VIDEO_CONFERENCE) && + participant.type !== ParticipantType.AUDIENCE; + + const needSlot = componentsNeedSlot || videoNeedSlot; + + if ((participant.slot?.index === null || !participant.slot) && needSlot) { + this.assignSlot(); + } + + if (participant.slot?.index !== null && !needSlot) { + this.setDefaultSlot(); + } + }; } From 074f84e35c5c48f987d202bcfb00500a261ffe1d Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 9 Jul 2024 17:06:27 -0300 Subject: [PATCH 08/32] fix: participant joined and list events payloads --- src/core/launcher/index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index f35dd8e7..8d279618 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -396,6 +396,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.logger.log('launcher service @ onParticipantLeave - participant left', presence.data); this.publish(ParticipantEvent.LEFT, presence.data); + this.publish(ParticipantEvent.LIST_UPDATED, Object.values(participantsMap)); }; /** @@ -422,17 +423,21 @@ export class Launcher extends Observable implements DefaultLauncher { } const { participants } = useStore(StoreType.GLOBAL); + const participant: Participant = { + id: presence.id, + name: presence.name, + timestamp: presence.timestamp, + ...presence.data, + }; if (!participants.value[presence.id]) { - this.publish(ParticipantEvent.JOINED, presence.data); + this.publish(ParticipantEvent.JOINED, participant); } - const participantsMap = { ...participants.value }; - participantsMap[presence.id] = { - ...presence.data, - ...participants.value[presence.id], - timestamp: presence.timestamp, - }; + const participantsMap = Object.assign({}, participants.value); + participantsMap[presence.id] = participant; + + if (isEqual(participantsMap, participants.value)) return; participants.publish(participantsMap); From d36fd04ee93059a11a7472e8b7f3c127cbb76319 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 10 Jul 2024 09:27:18 -0300 Subject: [PATCH 09/32] refactor: callback name --- src/core/launcher/index.ts | 8 ++++---- src/services/slot/index.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 8d279618..9284df54 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -48,7 +48,7 @@ export class Launcher extends Observable implements DefaultLauncher { localParticipant.publish({ ...participant }); participants.subscribe(this.onParticipantListUpdate); isDomainWhitelisted.subscribe(this.onAuthentication); - localParticipant.subscribe(this.onParticipantLocalParticipantUpdateOnStore); + localParticipant.subscribe(this.onLocalParticipantUpdateOnStore); group.publish(participantGroup); this.ioc = new IOC(localParticipant.value); @@ -243,12 +243,12 @@ export class Launcher extends Observable implements DefaultLauncher { }; /** - * @function onParticipantLocalParticipantUpdateOnStore - * @description on participant local participant update on store + * @function onLocalParticipantUpdateOnStore + * @description handles the update of the local participant in the store. * @param {Participant} participant - new participant data * @returns {void} */ - private onParticipantLocalParticipantUpdateOnStore = (participant: Participant): void => { + private onLocalParticipantUpdateOnStore = (participant: Participant): void => { this.participant = participant; }; diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index cde157dd..717fe8d0 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -22,7 +22,7 @@ export class SlotService { this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); const { localParticipant } = this.useStore(StoreType.GLOBAL); - localParticipant.subscribe(this.onParticipantLocalParticipantUpdateOnStore); + localParticipant.subscribe(this.onLocalParticipantUpdateOnStore); /** * When the participant enters the room, is setted the default slot @@ -165,12 +165,12 @@ export class SlotService { }; /** - * @function onParticipantLocalParticipantUpdateOnStore - * @description on participant local participant update on store + * @function onLocalParticipantUpdateOnStore + * @description handles the update of the local participant in the store. * @param {Participant} participant - new participant data * @returns {void} */ - private onParticipantLocalParticipantUpdateOnStore = (participant: Participant): void => { + private onLocalParticipantUpdateOnStore = (participant: Participant): void => { const COMPONENTS_THAT_NEED_SLOT = [ ComponentNames.FORM_ELEMENTS, ComponentNames.WHO_IS_ONLINE, From b81c809ec4d6815237ec6065a3540a2d19e1e005 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 10 Jul 2024 12:45:47 -0300 Subject: [PATCH 10/32] feat: implement connection limits by product instead of by room --- __mocks__/limits.mock.ts | 17 +++++++++++---- src/components/base/index.test.ts | 9 +++++++- src/components/base/index.ts | 4 +++- src/components/base/types.ts | 1 + src/components/comments/index.test.ts | 4 ++++ src/components/form-elements/index.test.ts | 2 ++ .../presence-mouse/canvas/index.test.ts | 2 ++ .../presence-mouse/html/index.test.ts | 3 +++ src/components/realtime/channel.test.ts | 14 +++++++------ src/components/realtime/channel.ts | 9 ++++++-- src/components/realtime/index.test.ts | 2 ++ src/components/realtime/index.ts | 4 ++-- src/components/types.ts | 2 +- src/components/video/index.test.ts | 3 +++ src/components/who-is-online/index.test.ts | 2 ++ src/core/index.ts | 21 +++++++++++++++---- src/core/launcher/index.test.ts | 17 +++++++++------ src/core/launcher/index.ts | 9 +++++--- src/services/io/index.ts | 9 +++++--- src/services/limits/index.test.ts | 16 +++++++++----- src/services/limits/index.ts | 9 ++++---- src/services/limits/types.ts | 16 ++++++++++---- 22 files changed, 129 insertions(+), 46 deletions(-) diff --git a/__mocks__/limits.mock.ts b/__mocks__/limits.mock.ts index a2e6f49b..1595e9a7 100644 --- a/__mocks__/limits.mock.ts +++ b/__mocks__/limits.mock.ts @@ -1,8 +1,17 @@ import { ComponentLimits } from '../src/services/limits/types'; export const LIMITS_MOCK: ComponentLimits = { - videoConference: true, - presence: true, - comments: true, - transcript: true, + presence: { + canUse: true, + maxParticipants: 50, + }, + realtime: { + canUse: true, + maxParticipants: 200, + }, + videoConference: { + canUse: true, + maxParticipants: 255, + canUseTranscript: true, + }, }; diff --git a/src/components/base/index.test.ts b/src/components/base/index.test.ts index 1ae04472..0f1d9730 100644 --- a/src/components/base/index.test.ts +++ b/src/components/base/index.test.ts @@ -13,6 +13,7 @@ import { useGlobalStore } from '../../services/stores'; import { ComponentNames } from '../types'; import { BaseComponent } from '.'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; class DummyComponent extends BaseComponent { protected logger: Logger; @@ -70,6 +71,7 @@ describe('BaseComponent', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); @@ -90,6 +92,7 @@ describe('BaseComponent', () => { ioc: new IOC(MOCK_LOCAL_PARTICIPANT), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); @@ -109,6 +112,7 @@ describe('BaseComponent', () => { ioc: new IOC(MOCK_LOCAL_PARTICIPANT), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); @@ -126,8 +130,9 @@ describe('BaseComponent', () => { config: null as unknown as Configuration, eventBus: null as unknown as EventBus, useStore: null as unknown as typeof useStore, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, }); - }).toThrowError(); + }).toThrow(); }); }); @@ -141,6 +146,7 @@ describe('BaseComponent', () => { ioc: new IOC(MOCK_LOCAL_PARTICIPANT), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); @@ -162,6 +168,7 @@ describe('BaseComponent', () => { ioc: new IOC(MOCK_LOCAL_PARTICIPANT), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); diff --git a/src/components/base/index.ts b/src/components/base/index.ts index e4a88efc..6d72e47e 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -15,6 +15,7 @@ import { DefaultAttachComponentOptions } from './types'; export abstract class BaseComponent extends Observable { public abstract name: ComponentNames; protected abstract logger: Logger; + protected connectionLimit: number | 'unlimited'; protected group: Group; protected ioc: IOC; protected eventBus: EventBus; @@ -51,7 +52,8 @@ export abstract class BaseComponent extends Observable { this.eventBus = eventBus; this.isAttached = true; this.ioc = ioc; - this.room = ioc.createRoom(this.name); + this.connectionLimit = params.connectionLimit ?? 50; + this.room = ioc.createRoom(this.name, this.connectionLimit); if (!hasJoinedRoom.value) { this.logger.log(`${this.name} @ attach - not joined yet`); diff --git a/src/components/base/types.ts b/src/components/base/types.ts index fbb08e4d..a2b25522 100644 --- a/src/components/base/types.ts +++ b/src/components/base/types.ts @@ -11,6 +11,7 @@ export interface DefaultAttachComponentOptions { eventBus: EventBus; useStore: (name: T) => Store; Presence3DManagerService: typeof Presence3DManager; + connectionLimit: number | 'unlimited'; } export type GlobalStore = { diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index 758eb9b2..3b065496 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -17,6 +17,7 @@ import { ComponentNames } from '../types'; import { PinAdapter, CommentsSide, Annotation, PinCoordinates } from './types'; import { Comments } from './index'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; const MOCK_PARTICIPANTS: ParticipantByGroupApi[] = [ { @@ -83,6 +84,7 @@ describe('Comments', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); @@ -337,6 +339,7 @@ describe('Comments', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); @@ -356,6 +359,7 @@ describe('Comments', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); diff --git a/src/components/form-elements/index.test.ts b/src/components/form-elements/index.test.ts index 80c55b47..55534d0d 100644 --- a/src/components/form-elements/index.test.ts +++ b/src/components/form-elements/index.test.ts @@ -10,6 +10,7 @@ import { ComponentNames } from '../types'; import { FieldEvents } from './types'; import { FormElements } from '.'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; describe('form elements', () => { let instance: any; @@ -32,6 +33,7 @@ describe('form elements', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); }); diff --git a/src/components/presence-mouse/canvas/index.test.ts b/src/components/presence-mouse/canvas/index.test.ts index a8ec8878..374d2cf9 100644 --- a/src/components/presence-mouse/canvas/index.test.ts +++ b/src/components/presence-mouse/canvas/index.test.ts @@ -1,6 +1,7 @@ import { MOCK_CANVAS } from '../../../../__mocks__/canvas.mock'; import { MOCK_CONFIG } from '../../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../../__mocks__/event-bus.mock'; +import { LIMITS_MOCK } from '../../../../__mocks__/limits.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import { useStore } from '../../../common/utils/use-store'; import { IOC } from '../../../services/io'; @@ -53,6 +54,7 @@ const createMousePointers = (): PointersCanvas => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index 2800cda5..b4090562 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -7,6 +7,7 @@ import { Presence3DManager } from '../../../services/presence-3d-manager'; import { ParticipantMouse } from '../types'; import { PointersHTML } from '.'; +import { LIMITS_MOCK } from '../../../../__mocks__/limits.mock'; const createMousePointers = (id: string = 'html'): PointersHTML => { const presenceMouseComponent = new PointersHTML(id); @@ -17,6 +18,7 @@ const createMousePointers = (id: string = 'html'): PointersHTML => { config: MOCK_CONFIG, Presence3DManagerService: Presence3DManager, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); @@ -379,6 +381,7 @@ describe('MousePointers on HTML', () => { eventBus: EVENT_BUS_MOCK, ioc: new IOC(MOCK_LOCAL_PARTICIPANT), Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); diff --git a/src/components/realtime/channel.test.ts b/src/components/realtime/channel.test.ts index 4df6d6e1..d49471a5 100644 --- a/src/components/realtime/channel.test.ts +++ b/src/components/realtime/channel.test.ts @@ -1,3 +1,4 @@ +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { IOC } from '../../services/io'; @@ -21,7 +22,8 @@ describe('Realtime Channel', () => { ChannelInstance = new Channel( 'channel', new IOC(MOCK_LOCAL_PARTICIPANT), - MOCK_LOCAL_PARTICIPANT + MOCK_LOCAL_PARTICIPANT, + LIMITS_MOCK.realtime.maxParticipants, ); ChannelInstance['state'] = RealtimeChannelState.CONNECTED; @@ -120,7 +122,7 @@ describe('Realtime Channel', () => { }); const h = await ChannelInstance.fetchHistory(); - + expect(spy).toHaveBeenCalled(); expect(h).toEqual({ 'unit-test-event-name': [ @@ -202,10 +204,10 @@ describe('Realtime Channel', () => { ChannelInstance['state'] = RealtimeChannelState.DISCONNECTED; ChannelInstance.disconnect(); - + expect(spy).not.toHaveBeenCalled(); }); - + test('Should log an error if a disconnect attempt is made when the channel is already disconnected', () => { const spy = jest.spyOn(ChannelInstance['logger'], 'log' as any); ChannelInstance['state'] = RealtimeChannelState.DISCONNECTED; @@ -213,6 +215,6 @@ describe('Realtime Channel', () => { ChannelInstance.disconnect(); expect(spy).toHaveBeenCalled(); - }) + }); }); -}) \ No newline at end of file +}); diff --git a/src/components/realtime/channel.ts b/src/components/realtime/channel.ts index 07e05baf..76e06bd3 100644 --- a/src/components/realtime/channel.ts +++ b/src/components/realtime/channel.ts @@ -20,13 +20,18 @@ export class Channel extends Observable { callback: (data: unknown) => void; }> = []; - constructor(name: string, ioc: IOC, localParticipant: Participant) { + constructor( + name: string, + ioc: IOC, + localParticipant: Participant, + connectionLimit: number | 'unlimited', + ) { super(); this.name = name; this.ioc = ioc; this.logger = new Logger('@superviz/sdk/realtime-channel'); - this.channel = this.ioc.createRoom(`realtime:${this.name}`); + this.channel = this.ioc.createRoom(`realtime:${this.name}`, connectionLimit); this.localParticipant = localParticipant; this.subscribeToRealtimeEvents(); diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index 0fa60c63..97d7d209 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -8,6 +8,7 @@ import { Presence3DManager } from '../../services/presence-3d-manager'; import { RealtimeComponentState } from './types'; import { Realtime } from '.'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; jest.mock('lodash/throttle', () => jest.fn((fn) => fn)); jest.useFakeTimers(); @@ -27,6 +28,7 @@ describe('realtime component', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.realtime.maxParticipants, useStore, }); diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index f034a22d..9fcc433c 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -55,7 +55,7 @@ export class Realtime extends BaseComponent { if (channel) return channel; - channel = new Channel(name, this.ioc, this.localParticipant); + channel = new Channel(name, this.ioc, this.localParticipant, this.connectionLimit); this.channels.set(name, channel); @@ -100,7 +100,7 @@ export class Realtime extends BaseComponent { protected start(): void { this.logger.log('started'); - this.channel = new Channel('default', this.ioc, this.localParticipant); + this.channel = new Channel('default', this.ioc, this.localParticipant, this.connectionLimit); this.channel.subscribe(RealtimeChannelEvent.REALTIME_CHANNEL_STATE_CHANGED, (state) => { if (state !== RealtimeChannelState.CONNECTED) return; diff --git a/src/components/types.ts b/src/components/types.ts index dd2cdb64..075d20dd 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -14,10 +14,10 @@ export enum ComponentNames { } export enum PresenceMap { + 'comments' = 'presence', 'presence3dMatterport' = 'presence', 'presence3dAutodesk' = 'presence', 'presence3dThreejs' = 'presence', - 'realtime' = 'presence', 'whoIsOnline' = 'presence', 'formElements' = 'presence', } diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index b41524c4..ae04ee0a 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -26,6 +26,7 @@ import { ParticipantToFrame } from './types'; import { VideoConference } from '.'; import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; Object.assign(global, { TextDecoder, TextEncoder }); @@ -94,6 +95,7 @@ describe('VideoConference', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); @@ -118,6 +120,7 @@ describe('VideoConference', () => { Presence3DManagerService: Presence3DManager, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, }); diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index 3d5ad70d..34d80bd4 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -17,6 +17,7 @@ import { Avatar, WhoIsOnlineParticipant, TooltipData } from './types'; import { WhoIsOnline } from './index'; import { MEETING_COLORS } from '../../common/types/meeting-colors.types'; +import { LIMITS_MOCK } from '../../../__mocks__/limits.mock'; const generateMockParticipant = ({ id, @@ -61,6 +62,7 @@ describe('Who Is Online', () => { config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, Presence3DManagerService: Presence3DManager, + connectionLimit: LIMITS_MOCK.presence.maxParticipants, useStore, }); diff --git a/src/core/index.ts b/src/core/index.ts index 1fd25576..100def89 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -107,9 +107,8 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { throw new Error('Failed to load configuration from server'); @@ -130,7 +129,21 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { describe('Components', () => { test('should not add component if realtime is not joined room', () => { - LimitsService.checkComponentLimit = jest.fn().mockReturnValue(true); + LimitsService.checkComponentLimit = jest.fn().mockReturnValue(LIMITS_MOCK.videoConference); const { hasJoinedRoom } = useStore(StoreType.GLOBAL); hasJoinedRoom.publish(false); @@ -73,7 +74,7 @@ describe('Launcher', () => { }); test('should add component', () => { - LimitsService.checkComponentLimit = jest.fn().mockReturnValue(true); + LimitsService.checkComponentLimit = jest.fn().mockReturnValue(LIMITS_MOCK.videoConference); LauncherInstance.addComponent(MOCK_COMPONENT); @@ -82,6 +83,7 @@ describe('Launcher', () => { ioc: expect.any(IOC), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + connectionLimit: LIMITS_MOCK.videoConference.maxParticipants, useStore, } as DefaultAttachComponentOptions), ); @@ -93,7 +95,10 @@ describe('Launcher', () => { }); test('should show a console message if limit reached and not add component', () => { - LimitsService.checkComponentLimit = jest.fn().mockReturnValue(false); + LimitsService.checkComponentLimit = jest.fn().mockReturnValue({ + ...LIMITS_MOCK.videoConference, + canUse: false, + }); LauncherInstance.addComponent(MOCK_COMPONENT); @@ -101,7 +106,7 @@ describe('Launcher', () => { }); test('should remove component', () => { - LimitsService.checkComponentLimit = jest.fn().mockReturnValue(true); + LimitsService.checkComponentLimit = jest.fn().mockReturnValue(LIMITS_MOCK.videoConference); LauncherInstance.addComponent(MOCK_COMPONENT); LauncherInstance.removeComponent(MOCK_COMPONENT); @@ -115,11 +120,11 @@ describe('Launcher', () => { test('should show a console message if component is not initialized yet', () => { LauncherInstance.removeComponent(MOCK_COMPONENT); - expect(MOCK_COMPONENT.detach).not.toBeCalled(); + expect(MOCK_COMPONENT.detach).not.toHaveBeenCalled(); }); test('should show a console message if component is already active', () => { - LimitsService.checkComponentLimit = jest.fn().mockReturnValue(true); + LimitsService.checkComponentLimit = jest.fn().mockReturnValue(LIMITS_MOCK.videoConference); LauncherInstance.addComponent(MOCK_COMPONENT); LauncherInstance.addComponent(MOCK_COMPONENT); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 9284df54..0598cd45 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -52,7 +52,7 @@ export class Launcher extends Observable implements DefaultLauncher { group.publish(participantGroup); this.ioc = new IOC(localParticipant.value); - this.room = this.ioc.createRoom('launcher'); + this.room = this.ioc.createRoom('launcher', 'unlimited'); // internal events without realtime this.eventBus = new EventBus(); @@ -79,12 +79,15 @@ export class Launcher extends Observable implements DefaultLauncher { return; } + const limit = LimitsService.checkComponentLimit(component.name); + component.attach({ ioc: this.ioc, config: config.configuration, eventBus: this.eventBus, useStore, Presence3DManagerService: Presence3DManager, + connectionLimit: limit.maxParticipants, }); this.activeComponents.push(component.name); @@ -191,7 +194,7 @@ export class Launcher extends Observable implements DefaultLauncher { */ private canAddComponent = (component: Partial): boolean => { const isProvidedFeature = config.get(`features.${component.name}`); - const hasComponentLimit = LimitsService.checkComponentLimit(component.name); + const componentLimit = LimitsService.checkComponentLimit(component.name); const isComponentActive = this.activeComponents.includes(component.name); const verifications = [ @@ -209,7 +212,7 @@ export class Launcher extends Observable implements DefaultLauncher { message: `Component ${component.name} is already active. Please remove it first`, }, { - isValid: hasComponentLimit, + isValid: componentLimit.canUse, message: `You reached the limit usage of ${component.name}`, }, ]; diff --git a/src/services/io/index.ts b/src/services/io/index.ts index a01b69ed..a5a5282d 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -99,11 +99,14 @@ export class IOC { /** * @function createRoom * @description create and join realtime room - * @param roomName {string} + * @param {string} roomName - name of the room that will be created + * @param {number | 'unlimited'} connectionLimit - + * connection limit for the room, the default is 50 because it's the maximum number of slots * @returns {Room} */ - public createRoom(roomName: string): Socket.Room { + public createRoom(roomName: string, connectionLimit: number | 'unlimited' = 50): Socket.Room { const roomId = config.get('roomId'); - return this.client.connect(`${roomId}:${roomName}`); + + return this.client.connect(`${roomId}:${roomName}`, connectionLimit); } } diff --git a/src/services/limits/index.test.ts b/src/services/limits/index.test.ts index c8eef768..fb3c913d 100644 --- a/src/services/limits/index.test.ts +++ b/src/services/limits/index.test.ts @@ -3,6 +3,7 @@ import { ComponentNames } from '../../components/types'; import config from '../config'; import LimitsService from './index'; +import { ComponentLimits } from './types'; describe('LimitsService', () => { describe('checkComponentLimit', () => { @@ -12,20 +13,25 @@ describe('LimitsService', () => { const result = LimitsService.checkComponentLimit(componentName); - expect(result).toBe(true); + expect(result).toBe(LIMITS_MOCK.videoConference); }); it('should return false if the component limit is exceeded', () => { const componentName = ComponentNames.COMMENTS; - jest.spyOn(config, 'get').mockReturnValue({ + const expected: ComponentLimits = { ...LIMITS_MOCK, - comments: false, - }); + presence: { + ...LIMITS_MOCK.presence, + canUse: false, + }, + }; + + jest.spyOn(config, 'get').mockReturnValue(expected); const result = LimitsService.checkComponentLimit(componentName); - expect(result).toBe(false); + expect(result).toBe(expected.presence); }); }); }); diff --git a/src/services/limits/index.ts b/src/services/limits/index.ts index 12c64457..7efe84f0 100644 --- a/src/services/limits/index.ts +++ b/src/services/limits/index.ts @@ -1,12 +1,13 @@ import { ComponentNames, PresenceMap } from '../../components/types'; import config from '../config'; -import { ComponentLimits } from './types'; +import { ComponentLimits, Limit, VideoConferenceLimit } from './types'; export default class LimitsService { - static checkComponentLimit(name: ComponentNames): boolean { - const componentName = PresenceMap[name] ?? name; + static checkComponentLimit(name: ComponentNames): Limit | VideoConferenceLimit { const limits = config.get('limits'); - return limits?.[componentName] ?? false; + const componentName = PresenceMap[name] ?? name; + + return limits?.[componentName] ?? { canUse: false, maxParticipants: 50 }; } } diff --git a/src/services/limits/types.ts b/src/services/limits/types.ts index e7284153..dc84873a 100644 --- a/src/services/limits/types.ts +++ b/src/services/limits/types.ts @@ -1,6 +1,14 @@ +export type Limit = { + canUse: boolean; + maxParticipants: number; +}; + +export type VideoConferenceLimit = Limit & { + canUseTranscript: boolean; +}; + export type ComponentLimits = { - videoConference: boolean; - presence: boolean; - comments: boolean; - transcript?: boolean; + videoConference: VideoConferenceLimit; + presence: Limit; + realtime: Limit; }; From ecec227f8ef6afa1d9c47832b7a5493bffc9c638 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 10 Jul 2024 14:52:17 -0300 Subject: [PATCH 11/32] fix: update active components list in the io --- src/core/launcher/index.ts | 7 +++++-- src/services/slot/index.test.ts | 3 +-- src/services/slot/index.ts | 5 ----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 0598cd45..cdad347d 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -98,6 +98,11 @@ export class Launcher extends Observable implements DefaultLauncher { activeComponents: this.activeComponents, }); + this.room.presence.update({ + ...localParticipant.value, + activeComponents: this.activeComponents, + }); + ApiService.sendActivity( localParticipant.value.id, group.value.id, @@ -348,9 +353,7 @@ export class Launcher extends Observable implements DefaultLauncher { Socket.PresenceEvents.JOINED_ROOM, this.onParticipantJoinedIOC, ); - this.room.presence.on(Socket.PresenceEvents.LEAVE, this.onParticipantLeaveIOC); - this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onParticipantUpdatedIOC); const { hasJoinedRoom } = useStore(StoreType.GLOBAL); diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 06357815..7998693e 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -141,8 +141,7 @@ describe('slot service', () => { activeComponents: [], }; - const { localParticipant } = useStore(StoreType.GLOBAL); - localParticipant.publish(event); + instance['onLocalParticipantUpdateOnStore'](event); expect(room.presence.update).toHaveBeenCalledWith({ slot: { diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 717fe8d0..2ed1820a 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -23,11 +23,6 @@ export class SlotService { const { localParticipant } = this.useStore(StoreType.GLOBAL); localParticipant.subscribe(this.onLocalParticipantUpdateOnStore); - - /** - * When the participant enters the room, is setted the default slot - */ - this.setDefaultSlot(); } /** From 5683f37fbc7e2ef8b388a905f0cc4ff2d3a52437 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 11 Jul 2024 11:46:36 -0300 Subject: [PATCH 12/32] feat: add target to es6 in the esm config --- .esbuild/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.esbuild/config.js b/.esbuild/config.js index 87da4b95..116af660 100644 --- a/.esbuild/config.js +++ b/.esbuild/config.js @@ -37,6 +37,7 @@ const esmConfig = { sourcemap: true, minify: true, splitting: true, + target: 'es6', format: 'esm', define: { global: 'window', 'process.env': JSON.stringify(env) }, }; From bf30bf416d0cd7c81a7a43eaf664a67b868c56bc Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 11 Jul 2024 15:05:52 -0300 Subject: [PATCH 13/32] fix: dont show default avatars list to audiences --- src/components/video/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 75e81f46..2f11953f 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -178,6 +178,9 @@ export class VideoConference extends BaseComponent { * @returns {void} */ private startVideo = (): void => { + const defaultAvatars = + this.params?.userType !== ParticipantType.AUDIENCE && this.params?.defaultAvatars === true; + this.videoConfig = { language: this.params?.language, canUseRecording: !!this.params?.enableRecording, @@ -185,8 +188,7 @@ export class VideoConference extends BaseComponent { canUseChat: !this.params?.chatOff, canUseCams: !this.params?.camsOff, canUseScreenshare: !this.params?.screenshareOff, - canUseDefaultAvatars: - !!this.params?.defaultAvatars && !this.localParticipant?.avatar?.model3DUrl, + canUseDefaultAvatars: defaultAvatars && !this.localParticipant?.avatar?.model3DUrl, canUseGather: !!this.params?.enableGather, canUseFollow: !!this.params?.enableFollow, canUseGoTo: !!this.params?.enableGoTo, From 669b29acc68640f6c65ddd28d68af985330c8255 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 11 Jul 2024 15:25:13 -0300 Subject: [PATCH 14/32] feat: add joined room prop to participant --- src/common/types/participant.types.ts | 4 ++++ src/components/video/index.test.ts | 2 ++ src/components/video/index.ts | 32 ++++++++++++++------------- src/services/roomState/index.ts | 10 ++++----- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/common/types/participant.types.ts b/src/common/types/participant.types.ts index 26e5efcb..9d141a85 100644 --- a/src/common/types/participant.types.ts +++ b/src/common/types/participant.types.ts @@ -28,6 +28,10 @@ export interface Participant { timestamp?: number; } +export interface VideoParticipant extends Participant { + joinedMeeting?: boolean; +} + export type ParticipantByGroupApi = { id: string; name: string; diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index cb174d09..1618524f 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -481,6 +481,7 @@ describe('VideoConference', () => { expect(VideoConferenceInstance['roomState'].updateMyProperties).toHaveBeenCalledWith({ name: 'John Doe', type: ParticipantType.HOST, + joinedMeeting: true, }); }); @@ -500,6 +501,7 @@ describe('VideoConference', () => { name: 'John Doe', avatar: MOCK_AVATAR, type: ParticipantType.HOST, + joinedMeeting: true, }); }); diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 2f11953f..770e39af 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -13,7 +13,7 @@ import { TranscriptState, } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; -import { Participant, ParticipantType } from '../../common/types/participant.types'; +import { VideoParticipant, ParticipantType } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; import { BrowserService } from '../../services/browser'; @@ -40,8 +40,8 @@ let KICK_PARTICIPANTS_TIMEOUT: ReturnType | null = null; export class VideoConference extends BaseComponent { public name: ComponentNames; protected logger: Logger; - private participantsOnMeeting: Partial[] = []; - private localParticipant: Participant; + private participantsOnMeeting: Partial[] = []; + private localParticipant: VideoParticipant; private videoManager: VideoConferenceManager; private connectionService: ConnectionService; private browserService: BrowserService; @@ -322,8 +322,8 @@ export class VideoConference extends BaseComponent { * @returns {Participant} a participant * */ private createParticipantFromPresence = ( - participant: PresenceEvent, - ): Participant => { + participant: PresenceEvent, + ): VideoParticipant => { return { id: participant.id, color: participant.data.slot?.color || MeetingColorsHex[16], @@ -495,7 +495,7 @@ export class VideoConference extends BaseComponent { * @param {Participant} participant - participant * @returns {void} */ - private onParticipantJoined = (participant: Participant): void => { + private onParticipantJoined = (participant: VideoParticipant): void => { this.logger.log('video conference @ on participant joined', participant); this.publish(MeetingEvent.MEETING_PARTICIPANT_JOINED, participant); @@ -507,6 +507,7 @@ export class VideoConference extends BaseComponent { avatar: participant.avatar, name: participant.name, type: participant.type, + joinedMeeting: true, }); return; @@ -515,6 +516,7 @@ export class VideoConference extends BaseComponent { this.roomState.updateMyProperties({ name: participant.name, type: participant.type, + joinedMeeting: true, }); }; @@ -524,7 +526,7 @@ export class VideoConference extends BaseComponent { * @param {Participant} _ - participant * @returns {void} */ - private onParticipantLeft = (_: Participant): void => { + private onParticipantLeft = (_: VideoParticipant): void => { this.logger.log('video conference @ on participant left', this.localParticipant); this.connectionService.removeListeners(); @@ -545,10 +547,10 @@ export class VideoConference extends BaseComponent { * @param {Record} participants - participants * @returns {void} */ - private onParticipantListUpdate = (participants: Record): void => { + private onParticipantListUpdate = (participants: Record): void => { this.logger.log('video conference @ on participant list update', participants); - const list: Participant[] = Object.values(participants).map((participant) => { + const list: VideoParticipant[] = Object.values(participants).map((participant) => { return { id: participant.id, color: participant.slot?.colorName || 'gray', @@ -672,7 +674,7 @@ export class VideoConference extends BaseComponent { * @param {Participant[]} participants - participants * @returns {void} */ - private onRealtimeParticipantsDidChange = (participants: Participant[]): void => { + private onRealtimeParticipantsDidChange = (participants: VideoParticipant[]): void => { this.logger.log('video conference @ on participants did change', participants); const participantList: ParticipandToFrame[] = participants.map((participant) => { return { @@ -753,7 +755,7 @@ export class VideoConference extends BaseComponent { * @param {PresenceEvent} participant - participant * @returns {void} */ - private onParticipantJoinedOnRealtime = (participant: PresenceEvent): void => { + private onParticipantJoinedOnRealtime = (participant: PresenceEvent): void => { this.logger.log('video conference @ on participant joined on realtime', participant); this.publish( @@ -772,7 +774,7 @@ export class VideoConference extends BaseComponent { * @param {PresenceEvent} participant * @returns {void} */ - private onParticipantLeftOnRealtime = (participant: PresenceEvent): void => { + private onParticipantLeftOnRealtime = (participant: PresenceEvent): void => { this.logger.log('video conference @ on participant left on realtime', participant); this.publish( @@ -781,7 +783,7 @@ export class VideoConference extends BaseComponent { ); }; - private onParticipantUpdateOnRealtime = (participant: PresenceEvent): void => { + private onParticipantUpdateOnRealtime = (participant: PresenceEvent): void => { this.logger.log('video conference @ on participant update on realtime', participant); const { localParticipant, participants } = this.useStore(StoreType.GLOBAL); @@ -881,9 +883,9 @@ export class VideoConference extends BaseComponent { } return current; - }, null) as Participant; + }, null) as VideoParticipant; - this.room.presence.update({ + this.room.presence.update({ ...this.localParticipant, isHost: host.id === this.localParticipant.id, }); diff --git a/src/services/roomState/index.ts b/src/services/roomState/index.ts index 33866d89..d0d041c3 100644 --- a/src/services/roomState/index.ts +++ b/src/services/roomState/index.ts @@ -7,7 +7,7 @@ import { } from '@superviz/socket-client'; import { TranscriptState } from '../../common/types/events.types'; -import { Participant, ParticipantType } from '../../common/types/participant.types'; +import { ParticipantType, VideoParticipant } from '../../common/types/participant.types'; import { RealtimeStateTypes } from '../../common/types/realtime.types'; import { StoreType } from '../../common/types/stores.types'; import { Logger, Observer } from '../../common/utils'; @@ -19,7 +19,7 @@ import { RoomPropertiesEvents, VideoRoomProperties } from './type'; export class RoomStateService { private room: Room; private logger: Logger; - private myParticipant: Partial = {}; + private myParticipant: Partial = {}; private localRoomProperties: VideoRoomProperties = null; private drawingData: DrawingData = null; private enableSync: boolean; @@ -70,12 +70,12 @@ export class RoomStateService { /** * @function updateMyProperties - * @param {Partial} Participant + * @param {Partial} newProperties * @description updates local participant properties * @returns {void} */ - public updateMyProperties = (newProperties?: Partial): void => { - const properties = newProperties ?? ({} as Participant); + public updateMyProperties = (newProperties?: Partial): void => { + const properties = newProperties ?? ({} as VideoParticipant); if (this.isMessageTooBig(properties) || this.left || !this.enableSync || this.isSyncFrozen) { return; From 3ac288bd74947aafa62c9f649276e3492cc66d01 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 11 Jul 2024 16:27:02 -0300 Subject: [PATCH 15/32] refactor: too many updates in the local participant when add or remove a component --- src/components/video/index.ts | 11 +------- src/core/launcher/index.test.ts | 41 +++++++++++++++++----------- src/core/launcher/index.ts | 47 +++------------------------------ src/services/slot/index.ts | 4 +-- 4 files changed, 32 insertions(+), 71 deletions(-) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index a44414e0..618dde82 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -308,8 +308,8 @@ export class VideoConference extends BaseComponent { localParticipant.subscribe((participant) => { this.localParticipant = { - ...participant, ...this.localParticipant, + ...participant, }; }); @@ -405,9 +405,6 @@ export class VideoConference extends BaseComponent { private onMeetingStateChange = (state: MeetingState): void => { this.logger.log('video conference @ on meeting state change', state); this.publish(MeetingEvent.MEETING_STATE_UPDATE, state); - - const { localParticipant } = this.useStore(StoreType.GLOBAL); - localParticipant.publish(localParticipant.value); }; /** @@ -804,12 +801,6 @@ export class VideoConference extends BaseComponent { MeetingEvent.MY_PARTICIPANT_UPDATED, this.createParticipantFromPresence(participant), ); - - localParticipant.publish({ - ...localParticipant.value, - ...participant.data, - type: this.params.userType as ParticipantType, - }); } participants.publish({ diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index aabc6e6d..11fa29f0 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -90,6 +90,17 @@ describe('Launcher', () => { const { localParticipant } = LauncherInstance['useStore'](StoreType.GLOBAL); + LauncherInstance['onParticipantUpdatedIOC']({ + connectionId: 'connection1', + data: { + ...MOCK_LOCAL_PARTICIPANT, + activeComponents: [MOCK_COMPONENT.name], + }, + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + timestamp: Date.now(), + }); + expect(localParticipant.value.activeComponents?.length).toBe(1); expect(localParticipant.value.activeComponents![0]).toBe(MOCK_COMPONENT.name); }); @@ -113,6 +124,17 @@ describe('Launcher', () => { const { localParticipant } = LauncherInstance['useStore'](StoreType.GLOBAL); + LauncherInstance['onParticipantUpdatedIOC']({ + connectionId: 'connection1', + data: { + ...MOCK_LOCAL_PARTICIPANT, + activeComponents: [], + }, + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + timestamp: Date.now(), + }); + expect(MOCK_COMPONENT.detach).toHaveBeenCalled(); expect(localParticipant.value.activeComponents?.length).toBe(0); }); @@ -191,32 +213,20 @@ describe('Launcher', () => { expect(LauncherInstance['publish']).toHaveBeenCalled(); }); - test('should update activeComponentsInstances when participant list is updated', () => { - LauncherInstance.addComponent(MOCK_COMPONENT); - - LauncherInstance['onParticipantListUpdate']({ - participant1: { - id: 'unit-test-participant-ably-id', - activeComponents: [MOCK_COMPONENT.name], - }, - }); - - expect(LauncherInstance['activeComponentsInstances'].length).toBe(1); - }); - test('should remove component when participant is not usign it anymore', () => { - LauncherInstance.addComponent(MOCK_COMPONENT); - LauncherInstance['onParticipantUpdatedIOC']({ connectionId: 'connection1', id: MOCK_LOCAL_PARTICIPANT.id, name: MOCK_LOCAL_PARTICIPANT.name as string, data: { ...MOCK_LOCAL_PARTICIPANT, + activeComponents: [], }, timestamp: Date.now(), }); + LauncherInstance.addComponent(MOCK_COMPONENT); + expect(LauncherInstance['activeComponentsInstances'].length).toBe(1); LauncherInstance.removeComponent(MOCK_COMPONENT); @@ -227,6 +237,7 @@ describe('Launcher', () => { name: MOCK_LOCAL_PARTICIPANT.name as string, data: { ...MOCK_LOCAL_PARTICIPANT, + activeComponents: LauncherInstance['activeComponents'], }, timestamp: Date.now(), }); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index cdad347d..cc23da62 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -46,7 +46,6 @@ export class Launcher extends Observable implements DefaultLauncher { ); localParticipant.publish({ ...participant }); - participants.subscribe(this.onParticipantListUpdate); isDomainWhitelisted.subscribe(this.onAuthentication); localParticipant.subscribe(this.onLocalParticipantUpdateOnStore); @@ -93,11 +92,6 @@ export class Launcher extends Observable implements DefaultLauncher { this.activeComponents.push(component.name); this.activeComponentsInstances.push(component); - localParticipant.publish({ - ...localParticipant.value, - activeComponents: this.activeComponents, - }); - this.room.presence.update({ ...localParticipant.value, activeComponents: this.activeComponents, @@ -150,10 +144,8 @@ export class Launcher extends Observable implements DefaultLauncher { return c.name !== component.name; }); - const { localParticipant } = this.useStore(StoreType.GLOBAL); - this.activeComponents.splice(this.activeComponents.indexOf(component.name), 1); - localParticipant.publish({ + this.room.presence.update({ ...this.participant, activeComponents: this.activeComponents, }); @@ -260,39 +252,6 @@ export class Launcher extends Observable implements DefaultLauncher { this.participant = participant; }; - /** - * @function onParticipantListUpdate - * @description on participant list update - * @param participants - participants list - * @returns {void} - */ - private onParticipantListUpdate = (participants: Record): void => { - this.logger.log('launcher service @ onParticipantListUpdate', participants); - const { localParticipant } = useStore(StoreType.GLOBAL); - - const participant: Participant = Object.values(participants).find( - (participant) => participant.id === localParticipant.value.id, - ); - - if (!participant || isEqual(localParticipant.value, participant)) return; - - localParticipant.publish({ - ...localParticipant.value, - ...participant, - }); - - this.activeComponentsInstances = this.activeComponentsInstances.filter((component) => { - /** - * @NOTE - Prevents removing all components when - * in the first update, activeComponents is undefined. - * It means we should keep all instances - */ - if (!participant.activeComponents) return true; - - return this.activeComponents.includes(component.name); - }); - }; - /** * @function onParticipantJoined * @description on participant joined @@ -416,14 +375,14 @@ export class Launcher extends Observable implements DefaultLauncher { if (localParticipant.value && presence.id === localParticipant.value.id) { localParticipant.publish({ - ...presence.data, ...localParticipant.value, + ...presence.data, timestamp: presence.timestamp, } as Participant); this.publish(ParticipantEvent.LOCAL_UPDATED, { - ...presence.data, ...localParticipant.value, + ...presence.data, }); this.logger.log('Publishing ParticipantEvent.UPDATED', presence.data); } diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 2ed1820a..a2378808 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -176,11 +176,11 @@ export class SlotService { ]; const componentsNeedSlot = COMPONENTS_THAT_NEED_SLOT.some((component) => { - return participant.activeComponents.includes(component); + return participant?.activeComponents?.includes(component); }); const videoNeedSlot = - participant.activeComponents.includes(ComponentNames.VIDEO_CONFERENCE) && + participant?.activeComponents?.includes(ComponentNames.VIDEO_CONFERENCE) && participant.type !== ParticipantType.AUDIENCE; const needSlot = componentsNeedSlot || videoNeedSlot; From 0ffa02d4769949a633776f90b22e11fc187df807 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 12 Jul 2024 12:52:53 -0300 Subject: [PATCH 16/32] fix: update global participant object when they enter the meeting room --- src/components/video/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 618dde82..0009b342 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -509,6 +509,23 @@ export class VideoConference extends BaseComponent { this.publish(MeetingEvent.MY_PARTICIPANT_JOINED, participant); this.kickParticipantsOnHostLeave = !this.params?.allowGuests; + const { localParticipant, participants } = this.useStore(StoreType.GLOBAL); + + const newParticipantName = participant.name.trim(); + + localParticipant.publish({ + ...localParticipant.value, + name: newParticipantName, + }); + + participants.publish({ + ...participants.value, + [participant.id]: { + ...localParticipant.value, + name: newParticipantName, + }, + }); + if (this.videoConfig.canUseDefaultAvatars) { this.roomState.updateMyProperties({ avatar: participant.avatar, From c2a881c379808ef278afbb9afc66171c89cce94a Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 12 Jul 2024 13:06:35 -0300 Subject: [PATCH 17/32] fix: internal video destroys don't remove component from activeComponents list --- src/components/video/index.ts | 19 +++++++++++++++++++ src/core/launcher/index.test.ts | 9 ++++++++- src/core/launcher/index.ts | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 0009b342..091d686c 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -553,6 +553,25 @@ export class VideoConference extends BaseComponent { private onParticipantLeft = (_: VideoParticipant): void => { this.logger.log('video conference @ on participant left', this.localParticipant); + const { localParticipant, participants } = this.useStore(StoreType.GLOBAL); + + localParticipant.publish({ + ...localParticipant.value, + activeComponents: localParticipant.value.activeComponents?.filter( + (ac) => ac !== ComponentNames.VIDEO_CONFERENCE, + ), + }); + + participants.publish({ + ...participants.value, + [this.localParticipant.id]: { + ...localParticipant.value, + activeComponents: localParticipant.value.activeComponents?.filter( + (ac) => ac !== ComponentNames.VIDEO_CONFERENCE, + ), + }, + }); + this.connectionService.removeListeners(); this.publish(MeetingEvent.DESTROY); this.publish(MeetingEvent.MY_PARTICIPANT_LEFT, this.localParticipant); diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 11fa29f0..c21994a9 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -149,6 +149,13 @@ describe('Launcher', () => { LimitsService.checkComponentLimit = jest.fn().mockReturnValue(LIMITS_MOCK.videoConference); LauncherInstance.addComponent(MOCK_COMPONENT); + + // it will be updated by IOC when the participant is updated + LauncherInstance['participant'] = { + ...MOCK_LOCAL_PARTICIPANT, + activeComponents: [MOCK_COMPONENT.name], + }; + LauncherInstance.addComponent(MOCK_COMPONENT); expect(MOCK_COMPONENT.attach).toHaveBeenCalledTimes(1); @@ -159,7 +166,7 @@ describe('Launcher', () => { LauncherInstance.addComponent(MOCK_COMPONENT); - expect(MOCK_COMPONENT.attach).not.toBeCalled(); + expect(MOCK_COMPONENT.attach).not.toHaveBeenCalled(); }); }); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index cc23da62..e91f4038 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -192,7 +192,7 @@ export class Launcher extends Observable implements DefaultLauncher { private canAddComponent = (component: Partial): boolean => { const isProvidedFeature = config.get(`features.${component.name}`); const componentLimit = LimitsService.checkComponentLimit(component.name); - const isComponentActive = this.activeComponents.includes(component.name); + const isComponentActive = this.participant.activeComponents?.includes(component.name); const verifications = [ { From b451aea33e4b7a067b1ed3f05faf14925aa313be Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 14 Jul 2024 10:27:41 -0300 Subject: [PATCH 18/32] fix: sync activeComponents list and instances with list inside the store --- src/core/launcher/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index e91f4038..1f063cbd 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -41,9 +41,7 @@ export class Launcher extends Observable implements DefaultLauncher { super(); this.logger = new Logger('@superviz/sdk/launcher'); - const { localParticipant, participants, group, isDomainWhitelisted } = this.useStore( - StoreType.GLOBAL, - ); + const { localParticipant, group, isDomainWhitelisted } = this.useStore(StoreType.GLOBAL); localParticipant.publish({ ...participant }); isDomainWhitelisted.subscribe(this.onAuthentication); @@ -192,7 +190,7 @@ export class Launcher extends Observable implements DefaultLauncher { private canAddComponent = (component: Partial): boolean => { const isProvidedFeature = config.get(`features.${component.name}`); const componentLimit = LimitsService.checkComponentLimit(component.name); - const isComponentActive = this.participant.activeComponents?.includes(component.name); + const isComponentActive = this.activeComponents?.includes(component.name); const verifications = [ { @@ -250,6 +248,13 @@ export class Launcher extends Observable implements DefaultLauncher { */ private onLocalParticipantUpdateOnStore = (participant: Participant): void => { this.participant = participant; + this.activeComponents = participant.activeComponents || []; + + if (this.activeComponents.length) { + this.activeComponentsInstances = this.activeComponentsInstances.filter((component) => { + return this.activeComponents.includes(component.name); + }); + } }; /** From 5ea65251fa3afd9eb634e3c8df0163049d8e9e47 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 15 Jul 2024 14:50:23 -0300 Subject: [PATCH 19/32] feat: skip devices callbacks call when the participant is audience --- src/components/video/index.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 091d686c..6738a339 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -90,6 +90,11 @@ export class VideoConference extends BaseComponent { * @returns {void} */ public toggleMicrophone(): void { + if (this.localParticipant.type === ParticipantType.AUDIENCE) { + console.warn('[SuperViz] Audience cannot toggle microphone'); + return; + } + return this.videoManager?.publishMessageToFrame(MeetingControlsEvent.TOGGLE_MICROPHONE); } @@ -99,6 +104,11 @@ export class VideoConference extends BaseComponent { * @returns {void} */ public toggleCam(): void { + if (this.localParticipant.type === ParticipantType.AUDIENCE) { + console.warn('[SuperViz] Audience cannot toggle camera'); + return; + } + this.videoManager?.publishMessageToFrame(MeetingControlsEvent.TOGGLE_CAM); } @@ -108,6 +118,11 @@ export class VideoConference extends BaseComponent { * @returns {void} */ public toggleScreenShare(): void { + if (this.localParticipant.type === ParticipantType.AUDIENCE) { + console.warn('[SuperViz] Audience cannot toggle screen share'); + return; + } + return this.videoManager?.publishMessageToFrame(MeetingControlsEvent.TOGGLE_SCREENSHARE); } @@ -126,6 +141,11 @@ export class VideoConference extends BaseComponent { * @returns {void} */ public toggleRecording(): void { + if (this.localParticipant.isHost) { + console.warn('[SuperViz] Only host can toggle recording'); + return; + } + return this.videoManager?.publishMessageToFrame(MeetingControlsEvent.TOGGLE_RECORDING); } From dd518a7523f95b6f0c81621d619ca8f36b854064 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 16 Jul 2024 15:25:16 -0300 Subject: [PATCH 20/32] fix: ensure user has the default slot when they enters the room --- src/core/launcher/index.ts | 48 ++++++++++++-------------- src/services/slot/index.test.ts | 46 +++---------------------- src/services/slot/index.ts | 61 ++++++++++++++++++++------------- 3 files changed, 64 insertions(+), 91 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 1c12618c..ca7367a0 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -51,6 +51,16 @@ export class Launcher extends Observable implements DefaultLauncher { this.ioc = new IOC(localParticipant.value); this.room = this.ioc.createRoom('launcher', 'unlimited'); + // Assign a slot to the participant + this.slotService = new SlotService(this.room, this.useStore); + localParticipant.publish({ + ...localParticipant.value, + slot: this.slotService.slot, + activeComponents: [], + }); + + this.participant = localParticipant.value; + // internal events without realtime this.eventBus = new EventBus(); @@ -65,10 +75,10 @@ export class Launcher extends Observable implements DefaultLauncher { * @param component - component to add * @returns {void} */ - public addComponent = (component: Partial): void => { + public addComponent = async (component: Partial): Promise => { if (!this.canAddComponent(component)) return; - const { hasJoinedRoom, localParticipant, group } = useStore(StoreType.GLOBAL); + const { hasJoinedRoom, group, localParticipant } = useStore(StoreType.GLOBAL); if (!hasJoinedRoom.value) { this.logger.log('launcher service @ addComponent - not joined yet'); @@ -90,17 +100,18 @@ export class Launcher extends Observable implements DefaultLauncher { this.activeComponents.push(component.name); this.activeComponentsInstances.push(component); + localParticipant.publish({ + ...this.participant, + activeComponents: this.activeComponents, + }); + this.room.presence.update({ - ...localParticipant.value, + ...this.participant, + slot: this.slotService.slot, activeComponents: this.activeComponents, }); - ApiService.sendActivity( - localParticipant.value.id, - group.value.id, - group.value.name, - component.name, - ); + ApiService.sendActivity(this.participant.id, group.value.id, group.value.name, component.name); }; /** @@ -267,19 +278,6 @@ export class Launcher extends Observable implements DefaultLauncher { } }; - /** - * @function onParticipantJoined - * @description on participant joined - * @param ablyParticipant - ably participant - * @returns {void} - */ - private onParticipantJoined = (participant: Socket.PresenceEvent): void => { - if (participant.id !== this.participant.id) return; - - this.logger.log('launcher service @ onParticipantJoined - local participant joined'); - this.attachComponentsAfterJoin(); - }; - private onSameAccount = (): void => { this.publish(ParticipantEvent.SAME_ACCOUNT_ERROR); this.destroy(); @@ -345,13 +343,11 @@ export class Launcher extends Observable implements DefaultLauncher { ): Promise => { if (presence.id !== this.participant.id) return; - // Assign a slot to the participant - this.slotService = new SlotService(this.room, this.useStore); - this.room.presence.update(this.participant); this.logger.log('launcher service @ onParticipantJoined - local participant joined'); - this.onParticipantJoined(presence); + + this.attachComponentsAfterJoin(); this.publish(ParticipantEvent.LOCAL_JOINED, this.participant); this.publish(ParticipantEvent.JOINED, this.participant); }; diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 7998693e..ff24b123 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -25,7 +25,7 @@ describe('slot service', () => { const instance = new SlotService(room, useStore); const result = await instance.assignSlot(); - expect(instance['slotIndex']).toBeDefined(); + expect(instance['slot']).toBeDefined(); expect(result).toEqual({ index: expect.any(Number), color: expect.any(String), @@ -44,10 +44,10 @@ describe('slot service', () => { } as any; const instance = new SlotService(room, useStore); - instance['slotIndex'] = 0; + instance['slot'].index = 0; instance.setDefaultSlot(); - expect(instance['slotIndex']).toBeNull(); + expect(instance['slot'].index).toBeNull(); expect(room.presence.update).toHaveBeenCalledWith({ slot: { index: null, @@ -75,7 +75,7 @@ describe('slot service', () => { const instance = new SlotService(room, useStore); await instance.assignSlot(); - expect(instance['slotIndex']).toBeNull(); + expect(console.error).toHaveBeenCalled(); }); test('if the slot is already in use, it should assign a new slot', async () => { @@ -108,7 +108,7 @@ describe('slot service', () => { const instance = new SlotService(room, useStore); const result = await instance.assignSlot(); - expect(instance['slotIndex']).toBeDefined(); + expect(instance['slot'].index).toBeDefined(); expect(result).toEqual({ index: expect.any(Number), color: expect.any(String), @@ -117,40 +117,4 @@ describe('slot service', () => { timestamp: expect.any(Number), }); }); - - test("should remove the slot from the participant when the participant don't need it anymore", async () => { - const room = { - presence: { - on: jest.fn(), - update: jest.fn(), - }, - } as any; - - const instance = new SlotService(room, useStore); - instance['slotIndex'] = 0; - - const event: Participant = { - ...MOCK_LOCAL_PARTICIPANT, - slot: { - index: 0, - color: MEETING_COLORS.turquoise, - colorName: MEETING_COLORS_KEYS[0], - textColor: '#000', - timestamp: 0, - }, - activeComponents: [], - }; - - instance['onLocalParticipantUpdateOnStore'](event); - - expect(room.presence.update).toHaveBeenCalledWith({ - slot: { - index: null, - color: expect.any(String), - textColor: expect.any(String), - colorName: expect.any(String), - timestamp: expect.any(Number), - }, - }); - }); }); diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index a2378808..9266dd9c 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -11,7 +11,13 @@ import { useStore } from '../../common/utils/use-store'; import { ComponentNames } from '../../components/types'; export class SlotService { - private slotIndex: number | null = null; + public slot: Slot = { + index: null, + color: MEETING_COLORS.gray, + textColor: '#fff', + colorName: 'gray', + timestamp: Date.now(), + }; constructor( private room: Socket.Room, @@ -20,9 +26,6 @@ export class SlotService { this.room = room; this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); - - const { localParticipant } = this.useStore(StoreType.GLOBAL); - localParticipant.subscribe(this.onLocalParticipantUpdateOnStore); } /** @@ -72,7 +75,7 @@ export class SlotService { timestamp: Date.now(), }; - this.slotIndex = slot; + this.slot = slotData; localParticipant.publish({ ...localParticipant.value, @@ -112,7 +115,7 @@ export class SlotService { timestamp: Date.now(), }; - this.slotIndex = slot.index; + this.slot = slot; localParticipant.publish({ ...localParticipant.value, @@ -133,39 +136,34 @@ export class SlotService { private onPresenceUpdate = async (event: Socket.PresenceEvent) => { const { localParticipant } = this.useStore(StoreType.GLOBAL); - if (!event.data.slot || !localParticipant.value?.slot?.index) return; - if (event.id === localParticipant.value.id) { + const slot = await this.validateSlotType(event.data); + localParticipant.publish({ ...localParticipant.value, - slot: event.data.slot, + slot: slot, }); - this.slotIndex = event.data.slot.index; + return; } - if (event.data.slot?.index === this.slotIndex) { - this.slotIndex = null; + if (event.data.slot?.index === null || this.slot.index === null) return; + + if (event.data.slot?.index === this.slot?.index) { + const slotData = await this.assignSlot(); + localParticipant.publish({ ...localParticipant.value, - slot: null, + slot: slotData, }); - const slotData = await this.assignSlot(); - console.debug( `[SuperViz] - Slot reassigned to ${localParticipant.value.id}, slot: ${slotData.colorName}`, ); } }; - /** - * @function onLocalParticipantUpdateOnStore - * @description handles the update of the local participant in the store. - * @param {Participant} participant - new participant data - * @returns {void} - */ - private onLocalParticipantUpdateOnStore = (participant: Participant): void => { + public participantNeedsSlot = (participant: Participant): boolean => { const COMPONENTS_THAT_NEED_SLOT = [ ComponentNames.FORM_ELEMENTS, ComponentNames.WHO_IS_ONLINE, @@ -185,12 +183,27 @@ export class SlotService { const needSlot = componentsNeedSlot || videoNeedSlot; - if ((participant.slot?.index === null || !participant.slot) && needSlot) { - this.assignSlot(); + return needSlot; + }; + + /** + * @function validateSlotType + * @description validate if the participant needs a slot + * @param {Participant} participant - new participant data + * @returns {void} + */ + private validateSlotType = async (participant: Participant): Promise => { + const needSlot = this.participantNeedsSlot(participant); + + if (participant.slot?.index === null && needSlot) { + const slotData = await this.assignSlot(); + this.slot = slotData; } if (participant.slot?.index !== null && !needSlot) { this.setDefaultSlot(); } + + return this.slot; }; } From cdc1d7a42e45174ad020060d801ebddadabb20a9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 16 Jul 2024 21:21:54 -0300 Subject: [PATCH 21/32] fix: wait to assign a slot before try again --- src/services/slot/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 9266dd9c..a178b4a6 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -11,6 +11,8 @@ import { useStore } from '../../common/utils/use-store'; import { ComponentNames } from '../../components/types'; export class SlotService { + private isAssigningSlot = false; + public slot: Slot = { index: null, color: MEETING_COLORS.gray, @@ -34,6 +36,9 @@ export class SlotService { * @returns void */ public async assignSlot(): Promise { + if (this.isAssigningSlot) return this.slot; + + this.isAssigningSlot = true; let slots = Array.from({ length: 50 }, (_, i) => i); let slot = Math.floor(Math.random() * 50); const { localParticipant, participants } = useStore(StoreType.GLOBAL); @@ -92,6 +97,7 @@ export class SlotService { this.room.presence.update({ slot: slotData }); + this.isAssigningSlot = false; return slotData; } catch (error) { console.error(error); @@ -193,6 +199,8 @@ export class SlotService { * @returns {void} */ private validateSlotType = async (participant: Participant): Promise => { + if (this.isAssigningSlot) return this.slot; + const needSlot = this.participantNeedsSlot(participant); if (participant.slot?.index === null && needSlot) { From 3e29e5ed05af7d2fa5c58deeb316ed51d71e1dd1 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 16 Jul 2024 21:22:23 -0300 Subject: [PATCH 22/32] fix: follow participant name and generate avatar when partcipant don't have it --- src/components/who-is-online/index.test.ts | 2 +- src/components/who-is-online/index.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index 34d80bd4..4a15bfe9 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -440,7 +440,7 @@ describe('Who Is Online', () => { expect(whoIsOnlineComponent['room'].emit).toHaveBeenCalledWith( WhoIsOnlineEvent.START_FOLLOW_ME, - event.detail.id, + event.detail, ); }); }); diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index 0e495e52..ad66c0ed 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -171,14 +171,27 @@ export class WhoIsOnline extends BaseComponent { this.room.presence.get((list) => { const dataList = list - .filter((participant) => participant.data['id'] && participant.data['avatar']) + .filter((participant) => participant.data['id']) .map(({ data }: { data: any }) => { + let avatar = data.avatar; + + if (!avatar) { + avatar = this.getAvatar({ + avatar: data.avatar, + color: data.slot.color, + name: data.name, + letterColor: data.slot.textColor, + }); + } + const tooltip = this.getTooltipData(data); const controls = this.getControls(data); + return { ...data, tooltip, controls, + avatar, isLocalParticipant: data.id === this.localParticipantId, }; }) as WhoIsOnlineParticipant[]; @@ -433,7 +446,7 @@ export class WhoIsOnline extends BaseComponent { private follow = ({ detail }: CustomEvent) => { const { everyoneFollowsMe } = this.useStore(StoreType.WHO_IS_ONLINE); everyoneFollowsMe.publish(!!detail?.id); - this.room.emit(WhoIsOnlineEvent.START_FOLLOW_ME, detail?.id); + this.room.emit(WhoIsOnlineEvent.START_FOLLOW_ME, detail); if (this.following) { this.publish(WhoIsOnlineEvent.START_FOLLOW_ME, this.following); From 5845baae197a92497335394a390ed6b488d9f8f8 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 17 Jul 2024 13:17:07 -0300 Subject: [PATCH 23/32] feat: update socket client --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c7657163..f7204b44 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.8.2", + "@superviz/socket-client": "1.9.0", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", "debug": "^4.3.4", diff --git a/yarn.lock b/yarn.lock index e5abc401..ce4a7be8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2666,10 +2666,10 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== -"@superviz/socket-client@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.8.2.tgz#054a19df95e144ae99f459ce75f38795feffbcb9" - integrity sha512-pB4Pq9GYL7iXFN5ppri9D5sG2ff5Yg/muBoT6pgW2scj91OL2741/ULuxcvTZiUtCW6H7ndHuplVMyHCczkIAA== +"@superviz/socket-client@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.9.0.tgz#fd14fb485e6b04275f810f2b90b38b62d8af972f" + integrity sha512-C6IkWJ6mWcL3y6cPvs2yVwTi0YZ/Nm76uYA+736X3vYOPsMna63Jl0nD+iNoIWj0l00hGAR5ZORTWLrMRnrAxA== dependencies: "@reactivex/rxjs" "^6.6.7" debug "^4.3.5" From b1c3a25ac8d03a96e4066bd0da0caac69acdc963 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 17 Jul 2024 14:43:05 -0300 Subject: [PATCH 24/32] fix: update socket client version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7204b44..0c60ecf3 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.9.0", + "@superviz/socket-client": "1.9.1", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", "debug": "^4.3.4", From 79e99d72167d31ac4ee8fc859ad96162825291b1 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 17 Jul 2024 15:09:43 -0300 Subject: [PATCH 25/32] fix(debug): don't force to reconect in the socket --- src/services/io/index.ts | 22 +++++++++++----------- yarn.lock | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/services/io/index.ts b/src/services/io/index.ts index ba5ce4b3..3256bb5f 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -35,17 +35,17 @@ export class IOC { } private handleConnectionState = (state: Socket.ConnectionState): void => { - const needsToReconnectStates = [ - Socket.ClientState.DISCONNECTED, - Socket.ClientState.RECONNECT_ERROR, - ]; - - if ( - needsToReconnectStates.includes(state.state) && - !['io client disconnect', 'Unauthorized connection'].includes(state.reason) - ) { - this.forceReconnect(); - } + // const needsToReconnectStates = [ + // Socket.ClientState.DISCONNECTED, + // Socket.ClientState.RECONNECT_ERROR, + // ]; + + // if ( + // needsToReconnectStates.includes(state.state) && + // !['io client disconnect', 'Unauthorized connection'].includes(state.reason) + // ) { + // this.forceReconnect(); + // } if (state.reason === 'Unauthorized connection') { console.error( diff --git a/yarn.lock b/yarn.lock index ce4a7be8..78a3baed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2666,10 +2666,10 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== -"@superviz/socket-client@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.9.0.tgz#fd14fb485e6b04275f810f2b90b38b62d8af972f" - integrity sha512-C6IkWJ6mWcL3y6cPvs2yVwTi0YZ/Nm76uYA+736X3vYOPsMna63Jl0nD+iNoIWj0l00hGAR5ZORTWLrMRnrAxA== +"@superviz/socket-client@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.9.1.tgz#392c370b049996dd7ea4d668ef9f69f3d8f7a123" + integrity sha512-esDtE/bSGNW1DeSuqv9/gE4tVDyaYxeQDrSeAlTA+rHQWPLOsOKyYE6r0SyNQJtThzgm0/VLtAGc+pGdcdSc8g== dependencies: "@reactivex/rxjs" "^6.6.7" debug "^4.3.5" From 0ef223aac9e3d04709c3f771b55bac3d14408c5c Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 18 Jul 2024 11:02:58 -0300 Subject: [PATCH 26/32] feat: add socket client library to SDK --- __mocks__/io.mock.ts | 2 +- jest.setup.js | 2 +- package.json | 4 +- src/components/base/index.ts | 2 +- src/components/form-elements/index.ts | 2 +- src/components/form-elements/types.ts | 2 +- src/components/presence-mouse/canvas/index.ts | 2 +- src/components/presence-mouse/html/index.ts | 2 +- src/components/realtime/channel.ts | 2 +- src/components/video/index.test.ts | 2 +- src/components/video/index.ts | 2 +- src/components/who-is-online/index.ts | 2 +- src/core/launcher/index.test.ts | 2 - src/core/launcher/index.ts | 2 +- .../socket/common/types/callbacks.types.ts | 1 + src/lib/socket/common/types/event.types.ts | 44 ++++ src/lib/socket/common/types/presence.types.ts | 8 + src/lib/socket/connection/index.ts | 127 +++++++++++ src/lib/socket/connection/types.ts | 41 ++++ src/lib/socket/index.ts | 23 ++ src/lib/socket/presence/index.ts | 196 +++++++++++++++++ src/lib/socket/presence/types.ts | 14 ++ src/lib/socket/realtime/index.ts | 62 ++++++ src/lib/socket/room/index.ts | 208 ++++++++++++++++++ src/lib/socket/room/types.ts | 39 ++++ src/services/io/index.test.ts | 43 ---- src/services/io/index.ts | 2 +- .../presence-3d-manager/index.test.ts | 2 +- src/services/presence-3d-manager/index.ts | 2 +- src/services/roomState/index.test.ts | 2 +- src/services/roomState/index.ts | 8 +- src/services/roomState/type.ts | 3 +- src/services/slot/index.ts | 2 +- 33 files changed, 785 insertions(+), 72 deletions(-) create mode 100644 src/lib/socket/common/types/callbacks.types.ts create mode 100644 src/lib/socket/common/types/event.types.ts create mode 100644 src/lib/socket/common/types/presence.types.ts create mode 100644 src/lib/socket/connection/index.ts create mode 100644 src/lib/socket/connection/types.ts create mode 100644 src/lib/socket/index.ts create mode 100644 src/lib/socket/presence/index.ts create mode 100644 src/lib/socket/presence/types.ts create mode 100644 src/lib/socket/realtime/index.ts create mode 100644 src/lib/socket/room/index.ts create mode 100644 src/lib/socket/room/types.ts diff --git a/__mocks__/io.mock.ts b/__mocks__/io.mock.ts index e38ffb8a..78795ee2 100644 --- a/__mocks__/io.mock.ts +++ b/__mocks__/io.mock.ts @@ -1,5 +1,5 @@ import { jest } from '@jest/globals'; -import { Room } from '@superviz/socket-client'; +import { Room } from '../src/lib/socket'; export const MOCK_IO = { ClientState: { diff --git a/jest.setup.js b/jest.setup.js index ff4045e5..e631e1b8 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -36,4 +36,4 @@ global.DOMPoint = class { } }; -jest.mock('@superviz/socket-client', () => MOCK_IO); +jest.mock('./src/lib/socket', () => MOCK_IO); diff --git a/package.json b/package.json index 0c60ecf3..a57ef0b2 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,9 @@ "lodash.isequal": "^4.5.0", "luxon": "^3.4.4", "rxjs": "^7.8.1", - "semantic-release-version-file": "^1.0.2" + "semantic-release-version-file": "^1.0.2", + "socket.io-client": "^4.7.5", + "zod": "^3.23.8" }, "config": { "commitizen": { diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 6d72e47e..cbcae7de 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { Group } from '../../common/types/participant.types'; diff --git a/src/components/form-elements/index.ts b/src/components/form-elements/index.ts index 01452c66..be3e5b44 100644 --- a/src/components/form-elements/index.ts +++ b/src/components/form-elements/index.ts @@ -1,4 +1,4 @@ -import { SocketEvent } from '@superviz/socket-client'; +import type { SocketEvent } from '../../lib/socket'; import { Participant } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; diff --git a/src/components/form-elements/types.ts b/src/components/form-elements/types.ts index 5d07277f..eacd4527 100644 --- a/src/components/form-elements/types.ts +++ b/src/components/form-elements/types.ts @@ -1,4 +1,4 @@ -import { SocketEvent } from '@superviz/socket-client'; +import { SocketEvent } from '../../lib/socket'; export type FormElementsProps = { fields?: string[] | string; diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index d8f328c5..3d04b265 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../../lib/socket'; import { throttle } from 'lodash'; import { RealtimeEvent } from '../../../common/types/events.types'; diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 4bffb602..0b47e02e 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../../lib/socket'; import { isEqual } from 'lodash'; import { Subscription, fromEvent, throttleTime } from 'rxjs'; diff --git a/src/components/realtime/channel.ts b/src/components/realtime/channel.ts index 76e06bd3..6775244f 100644 --- a/src/components/realtime/channel.ts +++ b/src/components/realtime/channel.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import throttle from 'lodash/throttle'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index fcc23042..1e51df9c 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -1,6 +1,6 @@ import { TextEncoder, TextDecoder } from 'util'; -import { PresenceEvent } from '@superviz/socket-client'; +import { PresenceEvent } from '../../lib/socket'; import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 6738a339..776663f4 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents, Room } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room } from '../../lib/socket'; import { ColorsVariables } from '../../common/types/colors.types'; import { diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index ad66c0ed..97b7bafb 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents } from '../../lib/socket'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; import { Participant, Avatar } from '../../common/types/participant.types'; diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index c21994a9..0eed5900 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -1,5 +1,3 @@ -import { PresenceEvent } from '@superviz/socket-client'; - import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index ca7367a0..50c1ac8c 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { isEqual } from 'lodash'; import { ParticipantEvent } from '../../common/types/events.types'; diff --git a/src/lib/socket/common/types/callbacks.types.ts b/src/lib/socket/common/types/callbacks.types.ts new file mode 100644 index 00000000..a97e7e2d --- /dev/null +++ b/src/lib/socket/common/types/callbacks.types.ts @@ -0,0 +1 @@ +export type ErrorCallback = (error: Error) => void; diff --git a/src/lib/socket/common/types/event.types.ts b/src/lib/socket/common/types/event.types.ts new file mode 100644 index 00000000..96dbc206 --- /dev/null +++ b/src/lib/socket/common/types/event.types.ts @@ -0,0 +1,44 @@ +/** + * @enum RoomEvents + * @description events that the server listens to in the room module + * @property JOIN_ROOM - event to join a room + * @property LEAVE_ROOM - event to leave a room + * @property UPDATE - event to update a room + * @property JOINED_ROOM - event to indicate a user has joined a room + * @property ERROR - event to indicate an error in the room module + */ +export enum RoomEvents { + JOIN_ROOM = 'room.join', + JOINED_ROOM = 'room.joined', + LEAVE_ROOM = 'room.leave', + UPDATE = 'room.update', + ERROR = 'room.error', +} + +export enum InternalRoomEvents { + GET = 'room.get', +} + +/** + * @enum PresenceEvents + * @description events that the server listens to in the presence module + * @property JOINED_ROOM - event to indicate a user has joined a room + * @property LEAVE - event to indicate a user has left a room + * @property UPDATE - event to indicate a user has updated their presence + * @property ERROR - event to indicate an error in the presence module + */ +export enum PresenceEvents { + JOINED_ROOM = 'presence.joined-room', + LEAVE = 'presence.leave', + ERROR = 'presence.error', + UPDATE = 'presence.update', +} + +/** + * @enum InternalPresenceEvents + * @description events that the server listens to in the presence module + * @property GET - event to get the presence list + */ +export enum InternalPresenceEvents { + GET = 'presence.get', +} diff --git a/src/lib/socket/common/types/presence.types.ts b/src/lib/socket/common/types/presence.types.ts new file mode 100644 index 00000000..2b600164 --- /dev/null +++ b/src/lib/socket/common/types/presence.types.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const PresenceSchema = z.object({ + id: z.string(), + name: z.string(), +}); + +export type Presence = z.infer; diff --git a/src/lib/socket/connection/index.ts b/src/lib/socket/connection/index.ts new file mode 100644 index 00000000..431bf3a9 --- /dev/null +++ b/src/lib/socket/connection/index.ts @@ -0,0 +1,127 @@ +import { Subject } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; + +import { ClientState, ConnectionState, SocketErrorEvent, SocketEvent } from './types'; +import { Logger } from '../../../common/utils'; + +export class ClientConnection { + private logger: Logger; + private stateObserver: Subject; + + public state: ClientState; + + constructor(private socket: Socket) { + this.logger = new Logger('@superviz/socket-client/connection'); + this.subscribeToManagerEvents(); + this.stateObserver = new Subject(); + } + + public on(next: (state: ConnectionState) => void, error?: ErrorCallback) { + if (this.stateObserver.closed) { + this.stateObserver = new Subject(); + } + + this.stateObserver.subscribe({ + next, + error, + }); + } + + public off() { + if (this.stateObserver.closed) return; + + this.stateObserver.unsubscribe(); + } + + /** + * @function subscribeToManagerEvents + * @description Subscribe to the manager events + * @returns {void} + */ + private subscribeToManagerEvents(): void { + this.socket.on('connect', this.onConnect); + this.socket.on('disconnect', this.onDisconnect); + this.socket.on('connect_error', this.onConnectError); + this.socket.io.on('error', this.onConnectionError); + this.socket.io.on('reconnect', this.onReconnect); + this.socket.io.on('reconnect_attempt', this.onReconnecAttempt); + this.socket.io.on('reconnect_error', this.onReconnectError); + this.socket.io.on('reconnect_failed', this.onReconnectFailed); + + // custom validations listener + this.socket.on(SocketEvent.ERROR, this.onCustomError); + } + + /** + * @function changeState + * @description Change the state of the connection + * @returns {void} + */ + private changeState(state: ClientState, reason?: string): void { + this.state = state; + + if (this.stateObserver.closed) return; + + this.stateObserver.next({ + state, + reason, + }); + } + + /** Manager events handlers */ + + private onConnect = () => { + this.logger.log('connection @ on connect', 'Connected to the socket'); + this.changeState(ClientState.CONNECTED); + }; + + private onDisconnect = (reason: Socket.DisconnectReason) => { + this.logger.log('connection @ on disconnect', 'Disconnected from the socket'); + this.changeState(ClientState.DISCONNECTED, reason); + }; + + private onConnectError = (error: Error) => { + this.logger.log('connection @ on connect error', 'Connection error', error); + this.changeState(ClientState.CONNECTION_ERROR, error.message); + }; + + private onConnectionError = (error: Error) => { + this.logger.log('connection @ on connection error', 'Connection error', error); + this.changeState(ClientState.CONNECTION_ERROR, error.message); + }; + + private onReconnect = () => { + this.logger.log('connection @ on reconnect', 'Reconnected to the socket'); + this.changeState(ClientState.CONNECTED); + }; + + private onReconnectError = (error: Error) => { + this.logger.log('connection @ on reconnect error', 'Reconnect error', error); + this.changeState(ClientState.RECONNECT_ERROR, error.message); + }; + + private onReconnectFailed = () => { + this.logger.log('connection @ on reconnect failed', 'Failed to reconnect to the socket'); + this.changeState(ClientState.RECONNECT_ERROR); + }; + + private onReconnecAttempt = (attempt: number) => { + this.logger.log('connection @ on reconnect attempt', `Reconnect attempt #${attempt}`); + this.changeState(ClientState.RECONNECTING, `Reconnect attempt #${attempt}`); + }; + + private onCustomError = (error: SocketErrorEvent) => { + if (error.needsToDisconnect) { + this.socket.disconnect(); + } + + if (error.level === 'error') { + console.error('[SuperViz - Error]', 'Type: ', error.errorType, 'Message :', error.message); + return; + } + + console.warn('[SuperViz - Warning]', 'Type: ', error.errorType, 'Message :', error.message); + }; +} diff --git a/src/lib/socket/connection/types.ts b/src/lib/socket/connection/types.ts new file mode 100644 index 00000000..dde82aab --- /dev/null +++ b/src/lib/socket/connection/types.ts @@ -0,0 +1,41 @@ +/** + * @enum ClientState + * @description the state of the client + * @property CONNECTED - the client is connected + * @property CONNECTING - the client is connecting + * @property DISCONNECTED - the client is disconnected + * @property CONNECTION_ERROR - the client has a connection error + * @property RECONNECTING - the client is reconnecting + * @property RECONNECT_ERROR - the client has a reconnect error + */ +export enum ClientState { + CONNECTED = 'CONNECTED', + CONNECTING = 'CONNECTING', + DISCONNECTED = 'DISCONNECTED', + CONNECTION_ERROR = 'CONNECTION_ERROR', + RECONNECTING = 'RECONNECTING', + RECONNECT_ERROR = 'RECONNECT_ERROR', +} + +/** + * @interface ConnectionState + * @description the state of the connection + * @property state - the state of the connection + * @property reason - the reason for the state change + */ +export interface ConnectionState { + state: ClientState; + reason?: string; +} + +export type SocketErrorEvent = { + errorType: 'message-size-limit' | 'rate-limit' | 'room-connections-limit'; + message: string; + connectionId: string; + needsToDisconnect: boolean; + level: 'error' | 'warn'; +}; + +export enum SocketEvent { + ERROR = 'socket-event.error', +} diff --git a/src/lib/socket/index.ts b/src/lib/socket/index.ts new file mode 100644 index 00000000..6a37879e --- /dev/null +++ b/src/lib/socket/index.ts @@ -0,0 +1,23 @@ +import { PresenceEvents, RoomEvents } from './common/types/event.types'; +import { ClientState, ConnectionState } from './connection/types'; +import { PresenceRoom } from './presence'; +import { PresenceCallback, PresenceEvent } from './presence/types'; +import { Realtime } from './realtime'; +import { Room } from './room'; +import { Callback, SocketEvent, JoinRoomPayload, RoomHistory } from './room/types'; + +export { + Realtime, + PresenceEvents, + RoomEvents, + ClientState, + ConnectionState, + Callback, + SocketEvent, + JoinRoomPayload, + RoomHistory, + PresenceCallback, + PresenceEvent, + Room, + PresenceRoom, +}; diff --git a/src/lib/socket/presence/index.ts b/src/lib/socket/presence/index.ts new file mode 100644 index 00000000..166736a3 --- /dev/null +++ b/src/lib/socket/presence/index.ts @@ -0,0 +1,196 @@ +import { Subject } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; +import { InternalPresenceEvents, PresenceEvents } from '../common/types/event.types'; +import type { Presence } from '../common/types/presence.types'; + +import { PresenceCallback, PresenceEvent, PresenceEventFromServer } from './types'; +import { Logger } from '../../../common/utils'; + +export class PresenceRoom { + private logger: Logger; + private presences: Set = new Set(); + private observers: Map> = new Map(); + + constructor(private io: Socket, private presence: Presence, private roomId: string) { + this.logger = new Logger('@superviz/socket-client/presence'); + + this.registerSubsjects(); + this.subscribeToPresenceEvents(); + } + + public static register(io: Socket, presence: Presence, roomId: string) { + return new PresenceRoom(io, presence, roomId); + } + + /** + * @function get + * @description Get the presences in the room + * @returns {void} + */ + public get(next: (data: PresenceEvent[]) => void, error?: ErrorCallback): void { + const subject = new Subject(); + + subject.subscribe({ + next, + error, + }); + + const callback = (event: { + presences: PresenceEventFromServer[]; + connectionId: string; + timestamp: number; + roomId: string; + }) => { + const presences = event.presences.map((presence) => ({ + connectionId: presence.connectionId, + data: presence.data, + id: presence.id, + name: presence.name, + timestamp: presence.timestamp, + })); + + this.logger.log('presence room @ get', event); + this.io.off(InternalPresenceEvents.GET, callback); + subject.next(presences); + subject.complete(); + }; + + this.io.on(InternalPresenceEvents.GET, callback); + this.io.emit(InternalPresenceEvents.GET, this.roomId); + } + + /** + * @function update + * @description update the presence data in the room + * @param payload - The data to update + * @returns {void} + */ + public update(payload: T): void { + const body: PresenceEvent = { + connectionId: this.io.id, + data: payload, + id: this.presence.id, + name: this.presence.name, + timestamp: Date.now(), + }; + + this.io.emit(PresenceEvents.UPDATE, this.roomId, body); + this.logger.log('presence room @ update', this.roomId, body); + } + + public destroy(): void { + this.io.off(PresenceEvents.JOINED_ROOM, this.onPresenceJoin); + this.io.off(PresenceEvents.LEAVE, this.onPresenceLeave); + this.io.off(PresenceEvents.UPDATE, this.onPresenceUpdate); + + this.observers.forEach((observer) => observer.unsubscribe()); + this.observers.clear(); + } + + /** + * @function registerSubsjects + * @description Register the subjects for the presence events + * @returns {void} + */ + private registerSubsjects(): void { + this.observers.set(PresenceEvents.JOINED_ROOM, new Subject()); + this.observers.set(PresenceEvents.LEAVE, new Subject()); + this.observers.set(PresenceEvents.UPDATE, new Subject()); + } + + /** + * @function on + * @description Listen to an event + * @param event - The event to listen to + * @param callback - The callback to execute when the event is emitted + * @returns {void} + */ + public on( + event: PresenceEvents, + callback: PresenceCallback, + error?: ErrorCallback, + ): void { + this.observers.get(event).subscribe({ + error, + next: callback, + }); + } + + /** + * @function off + * @description Stop listening to an event + * @param event - The event to stop listening to + * @param callback - The callback to remove from the event + * @returns {void} + */ + public off(event: PresenceEvents): void { + this.observers.get(event).unsubscribe(); + } + + /** + * @function subscribeToPresenceEvents + * @description Subscribe to the presence events + * @returns {void} + */ + private subscribeToPresenceEvents(): void { + this.io.on(PresenceEvents.JOINED_ROOM, this.onPresenceJoin); + this.io.on(PresenceEvents.LEAVE, this.onPresenceLeave); + this.io.on(PresenceEvents.UPDATE, this.onPresenceUpdate); + } + + /** + * @function onPresenceJoin + * @description Handle the presence join event + * @param event - The presence event + * @returns {void} + */ + private onPresenceJoin = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence join', event); + this.presences.add(event); + this.observers.get(PresenceEvents.JOINED_ROOM).next({ + connectionId: event.connectionId, + data: event.data, + id: event.id, + name: event.name, + timestamp: event.timestamp, + }); + }; + + /** + * @function onPresenceLeave + * @description Handle the presence leave event + * @param event - The presence event + * @returns {void} + */ + private onPresenceLeave = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence leave', event); + this.presences.delete(event); + this.observers.get(PresenceEvents.LEAVE).next(event); + }; + + /** + * @function onPresenceUpdate + * @description Handle the presence update event + * @param event - The presence event + * @returns {void} + */ + private onPresenceUpdate = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence update', event); + this.observers.get(PresenceEvents.UPDATE).next({ + connectionId: event.connectionId, + data: event.data, + id: event.id, + name: event.name, + roomId: event?.roomId, + timestamp: event.timestamp, + } as PresenceEvent); + }; +} diff --git a/src/lib/socket/presence/types.ts b/src/lib/socket/presence/types.ts new file mode 100644 index 00000000..89b986e3 --- /dev/null +++ b/src/lib/socket/presence/types.ts @@ -0,0 +1,14 @@ +export type PresenceEvent = { + id: string; + name: string; + connectionId: string; + data: T; + timestamp: number; +}; + +export interface PresenceEventFromServer extends PresenceEvent { + roomKey: string; + roomId: string; +} + +export type PresenceCallback = (event: PresenceEvent) => void; diff --git a/src/lib/socket/realtime/index.ts b/src/lib/socket/realtime/index.ts new file mode 100644 index 00000000..f45d54de --- /dev/null +++ b/src/lib/socket/realtime/index.ts @@ -0,0 +1,62 @@ +import { type Socket, Manager } from 'socket.io-client'; + +import { Presence } from '../common/types/presence.types'; +import { ClientConnection } from '../connection'; +import { ClientState } from '../connection/types'; +import { Room } from '../room'; + +export class Realtime { + private socket: Socket; + private manager: Manager; + public connection: ClientConnection; + + constructor( + private apiKey: string, + private environment: 'dev' | 'prod', + private presence: Presence, + ) { + this.manager = new Manager('https://io.superviz.com', { + addTrailingSlash: false, + secure: true, + withCredentials: true, + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + reconnectionAttempts: 5, + extraHeaders: { + 'sv-api-key': this.apiKey, + }, + }); + + // use the default namespace to handle the connections + const { origin } = window.location; + this.socket = this.manager.socket(`/${environment}`, { + auth: { + apiKey: this.apiKey, + origin, + envirioment: this.environment, + }, + }); + + this.connection = new ClientConnection(this.socket); + } + + public get state(): ClientState { + return this.connection.state; + } + + /** + * @function connect + * @param room - The room name + * @param maxConnections - The maximum number of connections allowed in the room + * @returns {Room} - The room instance + */ + public connect(room: string, maxConnections?: number | 'unlimited'): Room { + return Room.register(this.socket, this.presence, room, this.apiKey, maxConnections); + } + + public destroy() { + this.socket.disconnect(); + this.connection.off(); + } +} diff --git a/src/lib/socket/room/index.ts b/src/lib/socket/room/index.ts new file mode 100644 index 00000000..21933e82 --- /dev/null +++ b/src/lib/socket/room/index.ts @@ -0,0 +1,208 @@ +import { Subject, Subscription } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; +import { InternalRoomEvents, RoomEvents } from '../common/types/event.types'; +import { Presence } from '../common/types/presence.types'; +import { PresenceRoom } from '../presence'; + +import { Callback, SocketEvent, JoinRoomPayload, RoomHistory } from './types'; +import { Logger } from '../../../common/utils'; + +export class Room { + private logger: Logger; + private isJoined: boolean = false; + private subscriptions: Map, Subscription> = new Map(); + private observers: Map> = new Map(); + + public presence: PresenceRoom; + + constructor( + private io: Socket, + private user: Presence, + private roomId: string, + private apiKey: string, + private maxConnections: number | 'unlimited' = 100, + ) { + this.logger = new Logger('@superviz/socket-client/room'); + + const payload: JoinRoomPayload = { + name: roomId, + user, + maxConnections, + }; + + this.presence = PresenceRoom.register(io, user, roomId); + this.io.emit(RoomEvents.JOIN_ROOM, payload); + this.subscribeToRoomEvents(); + } + + public static register( + io: Socket, + presence: Presence, + roomId: string, + apiKey: string, + maxConnections: number | 'unlimited', + ): Room { + return new Room(io, presence, roomId, apiKey, maxConnections); + } + + /** + * @function on + * @description Listen to an event + * @param event - The event to listen to + * @param callback - The callback to execute when the event is emitted + * @returns {void} + */ + public on(event: string, callback: Callback): void { + this.logger.log('room @ on', event); + + let subject = this.observers.get(event); + + if (!subject) { + subject = new Subject(); + this.observers.set(event, subject); + + this.io.on(event, (data: SocketEvent) => { + this.publishEventToClient(event, data); + }); + } + + this.subscriptions.set(callback, subject.subscribe(callback)); + } + + /** + * @function off + * @description Stop listening to an event + * @param event - The event to stop listening to + * @param callback - The callback to remove from the event + * @returns {void} + */ + public off(event: string, callback?: Callback): void { + this.logger.log('room @ off', event); + + if (!callback) { + this.observers.delete(event); + this.io.off(event); + return; + } + + this.subscriptions.get(callback)?.unsubscribe(); + } + + /** + * @function emit + * @description Emit an event + * @param event - The event to emit + * @param payload - The payload to send with the event + * @returns {void} + */ + public emit(event: string, payload: T): void { + if (!this.isJoined) { + this.logger.log('Cannot emit event. Not joined to room'); + return; + } + + const body: SocketEvent = { + name: event, + roomId: this.roomId, + presence: this.user, + connectionId: this.io.id, + data: payload, + timestamp: Date.now(), + }; + + this.io.emit(RoomEvents.UPDATE, this.roomId, body); + this.logger.log('room @ emit', event, payload); + } + + /** + * @function get + * @description Get the presences in the room + * @returns {void} + */ + public history(next: (data: RoomHistory) => void, error?: ErrorCallback): void { + const subject = new Subject(); + + subject.subscribe({ + next, + error, + }); + + const callback = (event: RoomHistory) => { + this.logger.log('room @ history', event); + this.io.off(InternalRoomEvents.GET, callback); + subject.next(event); + subject.complete(); + subject.unsubscribe(); + }; + + this.io.on(InternalRoomEvents.GET, callback); + this.io.emit(InternalRoomEvents.GET, this.roomId); + } + + /** + * @function disconnect + * @description Disconnect from the room + * @returns {void} + */ + public disconnect(): void { + this.logger.log('room @ disconnect', 'Leaving room...', this.roomId); + this.io.emit(RoomEvents.LEAVE_ROOM, this.roomId); + + // unsubscribe from all events + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + this.subscriptions.clear(); + this.observers.forEach((subject) => subject.unsubscribe()); + this.observers.clear(); + + this.presence.destroy(); + } + + /** + * @function publishEventToClient + * @description Publish an event to the client + * @param event - The event to publish + * @param data - The data to publish + * @returns {void} + */ + private publishEventToClient(event: string, data: SocketEvent): void { + const subject = this.observers.get(event); + + if (!subject || data.roomId !== this.roomId) return; + + subject.next(data); + } + + /* + * @function subscribeToRoomEvents + * @description Subscribe to room events + * @returns {void} + */ + private subscribeToRoomEvents(): void { + this.io.on(RoomEvents.JOINED_ROOM, this.onJoinedRoom); + this.io.on(RoomEvents.ERROR, (event: SocketEvent) => { + this.logger.log('Error:', event.data); + }); + + this.io.on(`http:${this.roomId}:${this.apiKey}`, this.onHttpEvent); + } + + /** + * @function onJoinedRoom + * @description handles the event when a user joins a room. + * @param event The socket event containing presence data. + * @returns {void} + */ + private onJoinedRoom = (event: SocketEvent<{ name: string }>): void => { + if (this.roomId !== event?.data?.name) return; + + this.isJoined = true; + this.io.emit(RoomEvents.JOINED_ROOM, this.roomId, event.data); + this.logger.log('room @ joined', event); + }; + + private onHttpEvent = (event: SocketEvent) => { + this.publishEventToClient(event.name, event); + }; +} diff --git a/src/lib/socket/room/types.ts b/src/lib/socket/room/types.ts new file mode 100644 index 00000000..e40f3c0b --- /dev/null +++ b/src/lib/socket/room/types.ts @@ -0,0 +1,39 @@ +import z from 'zod'; + +import { PresenceSchema } from '../common/types/presence.types'; + +export type SocketEvent = { + name: string; + roomId: string; + connectionId: string; + presence?: z.infer; + data: T; + timestamp: number; +}; + +export type RoomHistory = { + roomId: string; + room: { + id: string; + name: string; + userId: string; + apiKey: string; + createdAt: Date; + }; + events: SocketEvent[]; + connectionId: string; + timestamp: Date; +}; + +export type Callback = (event: SocketEvent) => void; + +export const JoinRoomSchema = z.object({ + name: z.string(), + user: z.object({ + id: z.string(), + name: z.string(), + }), + maxConnections: z.union([z.number(), z.literal('unlimited'), z.undefined()]), +}); + +export type JoinRoomPayload = z.infer; diff --git a/src/services/io/index.test.ts b/src/services/io/index.test.ts index c22240ae..ff024881 100644 --- a/src/services/io/index.test.ts +++ b/src/services/io/index.test.ts @@ -1,9 +1,5 @@ -import * as Socket from '@superviz/socket-client'; - import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; -import { IOCState } from './types'; - import { IOC } from '.'; describe('io', () => { @@ -30,43 +26,4 @@ describe('io', () => { expect(room).toHaveProperty('off'); expect(room).toHaveProperty('emit'); }); - - test('should force reconnect', () => { - const spy = jest.spyOn(instance as any, 'forceReconnect'); - const callback = jest.fn(); - - instance?.stateSubject.subscribe(callback); - instance?.['handleConnectionState']({ - state: Socket.ClientState.DISCONNECTED, - reason: '', - }); - - expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(IOCState.DISCONNECTED); - }); - - test('should not force reconnect if reason is Unauthorized connection', () => { - const spy = jest.spyOn(instance as any, 'forceReconnect'); - const callback = jest.fn(); - - instance?.stateSubject.subscribe(callback); - instance?.['handleConnectionState']({ - state: Socket.ClientState.DISCONNECTED, - reason: 'Unauthorized connection', - }); - - expect(callback).toHaveBeenCalledWith(IOCState.AUTH_ERROR); - expect(spy).not.toHaveBeenCalled(); - }); - - test('should not force reconnect if state is not DISCONNECTED or RECONNECT_ERROR', () => { - const spy = jest.spyOn(instance as any, 'forceReconnect'); - - instance?.['handleConnectionState']({ - state: Socket.ClientState.CONNECTED, - }); - - expect(spy).not.toHaveBeenCalled(); - }); }); diff --git a/src/services/io/index.ts b/src/services/io/index.ts index 3256bb5f..a92cd255 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { Subject } from 'rxjs'; import { Participant } from '../../common/types/participant.types'; diff --git a/src/services/presence-3d-manager/index.test.ts b/src/services/presence-3d-manager/index.test.ts index 5baa63dd..01fb912a 100644 --- a/src/services/presence-3d-manager/index.test.ts +++ b/src/services/presence-3d-manager/index.test.ts @@ -1,4 +1,4 @@ -import { PresenceEvents } from '@superviz/socket-client'; +import { PresenceEvents } from '../../lib/socket'; import { MOCK_IO } from '../../../__mocks__/io.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; diff --git a/src/services/presence-3d-manager/index.ts b/src/services/presence-3d-manager/index.ts index 269e1d4b..bc687123 100644 --- a/src/services/presence-3d-manager/index.ts +++ b/src/services/presence-3d-manager/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents, Room, SocketEvent } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room, SocketEvent } from '../../lib/socket'; import { throttle } from 'lodash'; import { Participant } from '../../common/types/participant.types'; diff --git a/src/services/roomState/index.test.ts b/src/services/roomState/index.test.ts index 627aacd5..a3c064a6 100644 --- a/src/services/roomState/index.test.ts +++ b/src/services/roomState/index.test.ts @@ -1,6 +1,6 @@ import { TextEncoder, TextDecoder } from 'util'; -import { PresenceEvent, PresenceEvents, RoomEvents } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents } from '../../lib/socket'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { TranscriptState } from '../../common/types/events.types'; diff --git a/src/services/roomState/index.ts b/src/services/roomState/index.ts index d0d041c3..fb15bc4c 100644 --- a/src/services/roomState/index.ts +++ b/src/services/roomState/index.ts @@ -1,10 +1,4 @@ -import { - PresenceEvent, - PresenceEvents, - Room, - RoomEvents, - SocketEvent, -} from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room, RoomEvents, SocketEvent } from '../../lib/socket'; import { TranscriptState } from '../../common/types/events.types'; import { ParticipantType, VideoParticipant } from '../../common/types/participant.types'; diff --git a/src/services/roomState/type.ts b/src/services/roomState/type.ts index d35c18bc..71d70aed 100644 --- a/src/services/roomState/type.ts +++ b/src/services/roomState/type.ts @@ -1,8 +1,7 @@ -import { PresenceEvent } from '@superviz/socket-client'; +import { PresenceEvent } from '../../lib/socket'; import { TranscriptState } from '../../common/types/events.types'; import { Participant } from '../../common/types/participant.types'; -import { DrawingData } from '../video-conference-manager/types'; export interface VideoRoomProperties { hostClientId?: string; diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index a178b4a6..7f592a0c 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { NAME_IS_WHITE_TEXT, From 6aeb7f02016a715c5bcac9b701f86620cb5351af Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 19 Jul 2024 13:31:47 -0300 Subject: [PATCH 27/32] feat: load limits from server --- src/core/index.ts | 19 +++---------------- src/services/api/index.test.ts | 20 +++++++++++--------- src/services/api/index.ts | 18 +++++++----------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index 100def89..a5a55b92 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -107,9 +107,10 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { throw new Error('Failed to load configuration from server'); }); @@ -129,21 +130,7 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { return { @@ -209,7 +209,7 @@ describe('ApiService', () => { const baseUrl = 'https://dev.nodeapi.superviz.com'; const response = await ApiService.fetchLimits(baseUrl, VALID_API_KEY); - expect(response).toEqual(CHECK_LIMITS_MOCK.usage); + expect(response).toEqual(CHECK_LIMITS_MOCK.limits); }); }); @@ -217,7 +217,9 @@ describe('ApiService', () => { test('should return the participants', async () => { const response = await ApiService.fetchParticipantsByGroup('any_group_id'); - expect(response).toEqual([{"avatar": null, "id": "any_user_id", "name": "any_name", "email": "any_email"}]); + expect(response).toEqual([ + { avatar: null, id: 'any_user_id', name: 'any_name', email: 'any_email' }, + ]); }); }); @@ -225,10 +227,10 @@ describe('ApiService', () => { test('should create a mention', async () => { const response = await ApiService.createMentions({ commentsId: 'any_comment_id', - participants: [] + participants: [], }); expect(response).toEqual({}); }); - }) + }); }); diff --git a/src/services/api/index.ts b/src/services/api/index.ts index f549bf3e..83a84f55 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -2,14 +2,14 @@ import { SuperVizSdkOptions } from '../../common/types/sdk-options.types'; import { doRequest } from '../../common/utils'; import { Annotation } from '../../components/comments/types'; import config from '../config'; - +import { ComponentLimits } from '../limits/types'; import { AnnotationParams, CommentParams, CreateOrUpdateParticipantParams, FetchAnnotationsParams, - MentionParams + MentionParams, } from './types'; export default class ApiService { @@ -32,11 +32,11 @@ export default class ApiService { return doRequest(url, 'POST', { apiKey }); } - static async fetchLimits(baseUrl: string, apikey: string) { - const path: string = '/user/check_limits'; + static async fetchLimits(baseUrl: string, apikey: string): Promise { + const path: string = '/user/check_limits_v2'; const url: string = this.createUrl(baseUrl, path); const result = await doRequest(url, 'GET', '', { apikey }); - return result.usage; + return result.limits; } static async fetchWaterMark(baseUrl: string, apiKey: string) { @@ -130,18 +130,14 @@ export default class ApiService { return doRequest(url, 'POST', body, { apikey }); } - static async fetchParticipantsByGroup( - groupId: string, - ) { + static async fetchParticipantsByGroup(groupId: string) { const path = `/groups/participants/${groupId}`; const baseUrl = config.get('apiUrl'); const url = this.createUrl(baseUrl, path, { take: 10000 }); return doRequest(url, 'GET', undefined, { apikey: config.get('apiKey') }); } - static async createMentions( - mentionParams: MentionParams - ) { + static async createMentions(mentionParams: MentionParams) { const path = '/mentions'; const baseUrl = config.get('apiUrl'); const url = this.createUrl(baseUrl, path); From 29ad1f6adf940a96fb676ca0f970b36ecb730fa3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 22 Jul 2024 18:46:46 -0300 Subject: [PATCH 28/32] feat: add same account validation --- src/core/launcher/index.ts | 30 ++++++++++++++++++++-------- src/lib/socket/connection/index.ts | 3 ++- src/lib/socket/connection/types.ts | 6 +++++- src/services/io/index.ts | 32 ++++++++---------------------- src/services/io/types.ts | 1 + 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 50c1ac8c..af6d6ffb 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -184,6 +184,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.room?.presence.off(Socket.PresenceEvents.JOINED_ROOM); this.room?.presence.off(Socket.PresenceEvents.LEAVE); this.room?.presence.off(Socket.PresenceEvents.UPDATE); + this.ioc.stateSubject.unsubscribe(); this.ioc?.destroy(); this.isDestroyed = true; @@ -294,13 +295,8 @@ export class Launcher extends Observable implements DefaultLauncher { private startIOC = (): void => { this.logger.log('launcher service @ startIOC'); const { participants } = useStore(StoreType.GLOBAL); - // retrieve the current participants in the room - this.ioc.stateSubject.subscribe((state) => { - if (state === IOCState.AUTH_ERROR) { - this.onAuthentication(false); - } - }); + this.ioc.stateSubject.subscribe(this.onConnectionStateChange); this.room.presence.get((presences) => { const participantsMap: Record = {}; @@ -327,9 +323,24 @@ export class Launcher extends Observable implements DefaultLauncher { ); this.room.presence.on(Socket.PresenceEvents.LEAVE, this.onParticipantLeaveIOC); this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onParticipantUpdatedIOC); + }; - const { hasJoinedRoom } = useStore(StoreType.GLOBAL); - hasJoinedRoom.publish(true); + /** + * @function onConnectionStateChange + * @description on connection state change + * @param state - connection state + * @returns {void} + */ + private onConnectionStateChange = (state: IOCState): void => { + if (state === IOCState.AUTH_ERROR) { + this.onAuthentication(false); + return; + } + + if (state === IOCState.SAME_ACCOUNT_ERROR) { + this.onSameAccount(); + return; + } }; /** @@ -347,6 +358,9 @@ export class Launcher extends Observable implements DefaultLauncher { this.logger.log('launcher service @ onParticipantJoined - local participant joined'); + const { hasJoinedRoom } = useStore(StoreType.GLOBAL); + hasJoinedRoom.publish(true); + this.attachComponentsAfterJoin(); this.publish(ParticipantEvent.LOCAL_JOINED, this.participant); this.publish(ParticipantEvent.JOINED, this.participant); diff --git a/src/lib/socket/connection/index.ts b/src/lib/socket/connection/index.ts index 6a1f8b3f..cab6906d 100644 --- a/src/lib/socket/connection/index.ts +++ b/src/lib/socket/connection/index.ts @@ -3,7 +3,6 @@ import type { Socket } from 'socket.io-client'; import { ErrorCallback } from '../common/types/callbacks.types'; - import { ClientState, ConnectionState, SocketErrorEvent, SocketEvent } from './types'; import { Logger } from '../../../common/utils'; @@ -118,6 +117,8 @@ export class ClientConnection { this.socket.disconnect(); } + this.changeState(ClientState.DISCONNECTED, error.errorType); + if (error.level === 'error') { console.error('[SuperViz - Error]', 'Type: ', error.errorType, 'Message :', error.message); return; diff --git a/src/lib/socket/connection/types.ts b/src/lib/socket/connection/types.ts index dde82aab..1fdde25e 100644 --- a/src/lib/socket/connection/types.ts +++ b/src/lib/socket/connection/types.ts @@ -29,7 +29,11 @@ export interface ConnectionState { } export type SocketErrorEvent = { - errorType: 'message-size-limit' | 'rate-limit' | 'room-connections-limit'; + errorType: + | 'message-size-limit' + | 'rate-limit' + | 'room-connections-limit' + | 'user-already-in-room'; message: string; connectionId: string; needsToDisconnect: boolean; diff --git a/src/services/io/index.ts b/src/services/io/index.ts index a92cd255..1c3e7b27 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -35,18 +35,6 @@ export class IOC { } private handleConnectionState = (state: Socket.ConnectionState): void => { - // const needsToReconnectStates = [ - // Socket.ClientState.DISCONNECTED, - // Socket.ClientState.RECONNECT_ERROR, - // ]; - - // if ( - // needsToReconnectStates.includes(state.state) && - // !['io client disconnect', 'Unauthorized connection'].includes(state.reason) - // ) { - // this.forceReconnect(); - // } - if (state.reason === 'Unauthorized connection') { console.error( '[Superviz] Unauthorized connection. Please check your API key and if your domain is white listed.', @@ -62,22 +50,18 @@ export class IOC { return; } + console.log('[Superviz] Connection state:', state); + + if (state.reason === 'user-already-in-room') { + this.state = state; + this.stateSubject.next(IOCState.SAME_ACCOUNT_ERROR); + return; + } + this.state = state; this.stateSubject.next(state.state as unknown as IOCState); }; - /** - * @function forceReconnect - * @description force the socket to reconnect - * @returns {void} - */ - private forceReconnect(): void { - this.client?.destroy(); - this.client = null; - - this.createClient(); - } - /** * @function createClient * @description create a new socket client diff --git a/src/services/io/types.ts b/src/services/io/types.ts index 61514ccf..73d0f192 100644 --- a/src/services/io/types.ts +++ b/src/services/io/types.ts @@ -6,4 +6,5 @@ export enum IOCState { RECONNECTING = 'RECONNECTING', RECONNECT_ERROR = 'RECONNECT_ERROR', AUTH_ERROR = 'AUTH_ERROR', + SAME_ACCOUNT_ERROR = 'SAME_ACCOUNT_ERROR', } From fa3dd37fa503aca6f79c99565fcbe7843aa0c1af Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 22 Jul 2024 18:49:35 -0300 Subject: [PATCH 29/32] test: mock joined room --- src/core/launcher/index.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 0eed5900..2e262eee 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -50,6 +50,9 @@ describe('Launcher', () => { localParticipant.value = MOCK_LOCAL_PARTICIPANT; LauncherInstance = new Launcher(DEFAULT_INITIALIZATION_MOCK); + + const { hasJoinedRoom } = useStore(StoreType.GLOBAL); + hasJoinedRoom.publish(true); }); test('should be defined', () => { From 02a0b3e79daf367395f11a119b14f81e2a840dec Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 22 Jul 2024 19:48:15 -0300 Subject: [PATCH 30/32] fix: only change state to disconnected when needs to disconnect --- src/lib/socket/connection/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/socket/connection/index.ts b/src/lib/socket/connection/index.ts index cab6906d..4b33db66 100644 --- a/src/lib/socket/connection/index.ts +++ b/src/lib/socket/connection/index.ts @@ -115,10 +115,9 @@ export class ClientConnection { private onCustomError = (error: SocketErrorEvent) => { if (error.needsToDisconnect) { this.socket.disconnect(); + this.changeState(ClientState.DISCONNECTED, error.errorType); } - this.changeState(ClientState.DISCONNECTED, error.errorType); - if (error.level === 'error') { console.error('[SuperViz - Error]', 'Type: ', error.errorType, 'Message :', error.message); return; From e5082a523903d9d02be0bd1b3a975b048fb44fc2 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 22 Jul 2024 19:58:10 -0300 Subject: [PATCH 31/32] refactor: improve console messages --- src/lib/socket/connection/index.ts | 9 +++++++-- src/services/io/index.ts | 2 -- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/socket/connection/index.ts b/src/lib/socket/connection/index.ts index 4b33db66..9affaa34 100644 --- a/src/lib/socket/connection/index.ts +++ b/src/lib/socket/connection/index.ts @@ -118,11 +118,16 @@ export class ClientConnection { this.changeState(ClientState.DISCONNECTED, error.errorType); } + const logMessage = `[SuperViz] + - Error: ${error.errorType} + - Message: ${error.message} + `; + if (error.level === 'error') { - console.error('[SuperViz - Error]', 'Type: ', error.errorType, 'Message :', error.message); + console.error(logMessage); return; } - console.warn('[SuperViz - Warning]', 'Type: ', error.errorType, 'Message :', error.message); + console.warn(logMessage); }; } diff --git a/src/services/io/index.ts b/src/services/io/index.ts index 1c3e7b27..fce6b783 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -50,8 +50,6 @@ export class IOC { return; } - console.log('[Superviz] Connection state:', state); - if (state.reason === 'user-already-in-room') { this.state = state; this.stateSubject.next(IOCState.SAME_ACCOUNT_ERROR); From 72237a8f906290a47b1ae3ee812dab2de9c2c6a2 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 23 Jul 2024 09:54:18 -0300 Subject: [PATCH 32/32] feat(core): validate if the user id and room id follow the right pattern --- src/core/index.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/core/index.ts | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/core/index.test.ts b/src/core/index.test.ts index ddb7bfa3..f3042fd1 100644 --- a/src/core/index.test.ts +++ b/src/core/index.test.ts @@ -149,4 +149,44 @@ describe('initialization errors', () => { 'Color sv-primary-900 is not a valid color variable value. Please check the documentation for more information.', ); }); + + test('should throw an error if room id is invalid', async () => { + await expect( + sdk(UNIT_TEST_API_KEY, { + ...SIMPLE_INITIALIZATION_MOCK, + roomId: '', + }), + ).rejects.toThrow( + '[SuperViz] Room id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); + + await expect( + sdk(UNIT_TEST_API_KEY, { + ...SIMPLE_INITIALIZATION_MOCK, + roomId: '1', + }), + ).rejects.toThrow( + '[SuperViz] Room id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); + }); + + test('should throw an error if participant id is invalid', async () => { + await expect( + sdk(UNIT_TEST_API_KEY, { + ...SIMPLE_INITIALIZATION_MOCK, + participant: { ...SIMPLE_INITIALIZATION_MOCK.participant, id: '' }, + }), + ).rejects.toThrow( + '[SuperViz] Participant id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); + + await expect( + sdk(UNIT_TEST_API_KEY, { + ...SIMPLE_INITIALIZATION_MOCK, + participant: { ...SIMPLE_INITIALIZATION_MOCK.participant, id: '1' }, + }), + ).rejects.toThrow( + '[SuperViz] Participant id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); + }); }); diff --git a/src/core/index.ts b/src/core/index.ts index a5a55b92..dfa89e42 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -10,6 +10,27 @@ import RemoteConfigService from '../services/remote-config-service'; import LauncherFacade from './launcher'; import { LauncherFacade as LauncherFacadeType } from './launcher/types'; +/** + * @function validateId + * @description validate if the id follows the constraints + * @param {string} id - id to validate + * @returns {boolean} + */ +function validateId(id: string): boolean { + const lengthConstraint = /^.{2,64}$/; + const pattern = /^[-_&@+=,(){}\[\]\/«».:|'"#a-zA-Z0-9À-ÿ\s]*$/; + + if (!lengthConstraint.test(id)) { + return false; + } + + if (!pattern.test(id)) { + return false; + } + + return true; +} + /** * @function validateOptions * @description Validate the options passed to the SDK @@ -27,15 +48,27 @@ const validateOptions = ({ } if (!group || !group.name || !group.id) { - throw new Error('Group fields is required'); + throw new Error('[SuperViz] Group fields is required'); } if (!participant || !participant.id || !participant.name) { - throw new Error('Participant name and id is required'); + throw new Error('[SuperViz] Participant name and id is required'); } if (!roomId) { - throw new Error('Room id is required'); + throw new Error('[SuperViz] Room id is required'); + } + + if (!validateId(roomId)) { + throw new Error( + '[SuperViz] Room id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); + } + + if (!validateId(participant.id)) { + throw new Error( + '[SuperViz] Participant id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"', + ); } };