diff --git a/package.json b/package.json index cde7a13f..c7657163 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.8.0", + "@superviz/socket-client": "1.8.2", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", "debug": "^4.3.4", diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 618dde82..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); } @@ -509,6 +529,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, @@ -536,6 +573,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/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); 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..ca7367a0 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); @@ -53,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(); @@ -67,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'); @@ -92,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); }; /** @@ -192,7 +201,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.activeComponents?.includes(component.name); const verifications = [ { @@ -242,6 +251,16 @@ export class Launcher extends Observable implements DefaultLauncher { ); }; + private onLocalParticipantUpdate = (participant: Participant): void => { + this.activeComponents = participant.activeComponents || []; + + if (this.activeComponents.length) { + this.activeComponentsInstances = this.activeComponentsInstances.filter((ac) => { + return this.activeComponents.includes(ac.name); + }); + } + }; + /** * @function onLocalParticipantUpdateOnStore * @description handles the update of the local participant in the store. @@ -250,19 +269,13 @@ export class Launcher extends Observable implements DefaultLauncher { */ private onLocalParticipantUpdateOnStore = (participant: Participant): void => { this.participant = participant; - }; + this.activeComponents = participant.activeComponents || []; - /** - * @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(); + if (this.activeComponents.length) { + this.activeComponentsInstances = this.activeComponentsInstances.filter((component) => { + return this.activeComponents.includes(component.name); + }); + } }; private onSameAccount = (): void => { @@ -330,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/io/index.ts b/src/services/io/index.ts index a5a5282d..ba5ce4b3 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -23,7 +23,6 @@ export class IOC { */ public destroy(): void { this.client.destroy(); - this.client.connection.off(); } /** @@ -43,7 +42,7 @@ export class IOC { if ( needsToReconnectStates.includes(state.state) && - state.reason !== 'Unauthorized connection' + !['io client disconnect', 'Unauthorized connection'].includes(state.reason) ) { this.forceReconnect(); } 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..a178b4a6 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -11,7 +11,15 @@ import { useStore } from '../../common/utils/use-store'; import { ComponentNames } from '../../components/types'; export class SlotService { - private slotIndex: number | null = null; + private isAssigningSlot = false; + + public slot: Slot = { + index: null, + color: MEETING_COLORS.gray, + textColor: '#fff', + colorName: 'gray', + timestamp: Date.now(), + }; constructor( private room: Socket.Room, @@ -20,9 +28,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); } /** @@ -31,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); @@ -72,7 +80,7 @@ export class SlotService { timestamp: Date.now(), }; - this.slotIndex = slot; + this.slot = slotData; localParticipant.publish({ ...localParticipant.value, @@ -89,6 +97,7 @@ export class SlotService { this.room.presence.update({ slot: slotData }); + this.isAssigningSlot = false; return slotData; } catch (error) { console.error(error); @@ -112,7 +121,7 @@ export class SlotService { timestamp: Date.now(), }; - this.slotIndex = slot.index; + this.slot = slot; localParticipant.publish({ ...localParticipant.value, @@ -133,39 +142,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 +189,29 @@ 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 => { + if (this.isAssigningSlot) return this.slot; + + 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; }; } diff --git a/yarn.lock b/yarn.lock index 38bd3540..e5abc401 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2666,18 +2666,18 @@ 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.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.8.0.tgz#6e21f177bc3f5ed128784f14a95a3c4f59d40593" - integrity sha512-+jTpYYug8rgugaoBvgfa3GU/zCSi00grwZzwCDZkLXTxil7e3psiZyRUYH9HucPRUT8ULgpplDN5MF9G43E/fQ== +"@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== dependencies: "@reactivex/rxjs" "^6.6.7" - debug "^4.3.4" + debug "^4.3.5" lodash "^4.17.21" rxjs "^7.8.1" semantic-release-version-file "^1.0.2" - socket.io-client "^4.7.4" - zod "^3.22.4" + socket.io-client "^4.7.5" + zod "^3.23.8" "@tootallnate/once@2": version "2.0.0" @@ -4776,7 +4776,7 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1: +debug@^4.3.1, debug@^4.3.5: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -9958,10 +9958,10 @@ smart-buffer@^4.2.0: resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socket.io-client@^4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8" - integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg== +socket.io-client@^4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" + integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" @@ -11301,7 +11301,7 @@ yoctocolors@^2.0.0: resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.0.2.tgz#8e871e30d7eabb1976776e07a9fe2fe9a8c46fba" integrity sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw== -zod@^3.22.4: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==