From cff257b4f3cd90bec71c133b4b84ffd880335e55 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 1 Mar 2024 10:45:20 -0300 Subject: [PATCH 01/83] feat: create base subject that will be data in stores --- src/services/stores/common/types.ts | 20 +++++++ src/services/stores/common/utils.ts | 13 +++++ src/services/stores/subject/index.test.ts | 0 src/services/stores/subject/index.ts | 66 +++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 src/services/stores/common/types.ts create mode 100644 src/services/stores/common/utils.ts create mode 100644 src/services/stores/subject/index.test.ts create mode 100644 src/services/stores/subject/index.ts diff --git a/src/services/stores/common/types.ts b/src/services/stores/common/types.ts new file mode 100644 index 00000000..ffc5eea0 --- /dev/null +++ b/src/services/stores/common/types.ts @@ -0,0 +1,20 @@ +type callback = (a: T, b?: K) => void; + +export type SimpleSubject = { + value: T; + publish: callback; + subscribe: callback>; + unsubscribe: callback; +}; + +export type Singleton = { + set value(value: T); + get value(): T; +}; + +export type PublicSubject = { + get value(): T; + set value(T); + subscribe: callback>; + unsubscribe: callback; +}; diff --git a/src/services/stores/common/utils.ts b/src/services/stores/common/utils.ts new file mode 100644 index 00000000..cd7d6277 --- /dev/null +++ b/src/services/stores/common/utils.ts @@ -0,0 +1,13 @@ +import { Singleton } from './types'; + +export function CreateSingleton(): Singleton { + return { + set value(value: T) { + this.instance = value; + Object.freeze(this); + }, + get value() { + return this.instance; + }, + }; +} diff --git a/src/services/stores/subject/index.test.ts b/src/services/stores/subject/index.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/services/stores/subject/index.ts b/src/services/stores/subject/index.ts new file mode 100644 index 00000000..207a9b16 --- /dev/null +++ b/src/services/stores/subject/index.ts @@ -0,0 +1,66 @@ +import { BehaviorSubject, distinctUntilChanged, shareReplay } from 'rxjs'; +import type { Subscription } from 'rxjs'; + +import { PublicSubject } from '../common/types'; + +export class Subject { + public state: T; + private subject: BehaviorSubject; + private subscriptions: Map = new Map(); + + constructor(state: T, _subject: BehaviorSubject) { + this.state = state; + this.subject = _subject.pipe( + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }), + ) as any; + } + + private getValue(): T { + return this.state; + } + + private setValue(newValue: T): void { + this.state = newValue; + this.subject.next(this.state); + } + + public subscribe(subscriptionId: string, callback: (value: T) => void) { + const subscription = this.subject.subscribe(callback); + this.subscriptions.set(subscriptionId, subscription); + } + + public unsubscribe(subscriptionId: string) { + this.subscriptions.get(subscriptionId)?.unsubscribe(); + this.subscriptions.delete(subscriptionId); + } + + public destroy() { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + this.subscriptions.clear(); + } + + public expose(): PublicSubject { + const subscribe = this.subscribe.bind(this); + const unsubscribe = this.unsubscribe.bind(this); + const getter = this.getValue.bind(this); + const setter = this.setValue.bind(this); + + return { + get value() { + return getter(); + }, + set value(newValue: T) { + setter(newValue); + }, + subscribe, + unsubscribe, + }; + } +} + +export default function subject(initialState: T): Subject { + const subject = new BehaviorSubject(initialState); + + return new Subject(initialState, subject); +} From a6bcbc8674d1ca74ca1c1ffd7aa12c750f2dfeed Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 1 Mar 2024 11:11:04 -0300 Subject: [PATCH 02/83] feat: create global/launcher store --- src/services/stores/global/index.test.ts | 0 src/services/stores/global/index.ts | 37 ++++++++++++++++++++++++ src/services/stores/index.ts | 1 + 3 files changed, 38 insertions(+) create mode 100644 src/services/stores/global/index.test.ts create mode 100644 src/services/stores/global/index.ts create mode 100644 src/services/stores/index.ts diff --git a/src/services/stores/global/index.test.ts b/src/services/stores/global/index.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/services/stores/global/index.ts b/src/services/stores/global/index.ts new file mode 100644 index 00000000..6fe80843 --- /dev/null +++ b/src/services/stores/global/index.ts @@ -0,0 +1,37 @@ +import { Participant } from '../../../common/types/participant.types'; +import { Singleton } from '../common/types'; +import { CreateSingleton } from '../common/utils'; +import subject from '../subject'; + +const instance: Singleton = CreateSingleton(); + +class GlobalStore { + public localParticipant = subject(null); + public participants = subject([]); + + constructor() { + if (instance.value) { + throw new Error('CommentsStore is a singleton. There can only be one instance of it.'); + } + + instance.value = this; + } + + public destroy() { + this.localParticipant.destroy(); + instance.value = null; + } +} + +const store = new GlobalStore(); +const localParticipant = store.localParticipant.expose(); +const participants = store.participants.expose(); +const destroy = store.destroy.bind(store); + +export function useGlobalStore() { + return { + localParticipant, + participants, + destroy, + }; +} diff --git a/src/services/stores/index.ts b/src/services/stores/index.ts new file mode 100644 index 00000000..7b3e2335 --- /dev/null +++ b/src/services/stores/index.ts @@ -0,0 +1 @@ +export { useGlobalStore } from './global'; From b7bfae94d9c13f6bea461850ad6d862e03694910 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 1 Mar 2024 11:11:28 -0300 Subject: [PATCH 03/83] refactor: use global store --- src/core/launcher/index.ts | 47 ++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 3c682a97..1b7bfef2 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -12,6 +12,8 @@ import { EventBus } from '../../services/event-bus'; import LimitsService from '../../services/limits'; import { AblyRealtimeService } from '../../services/realtime'; import { AblyParticipant } from '../../services/realtime/ably/types'; +import { useGlobalStore } from '../../services/stores'; +import { PublicSubject } from '../../services/stores/common/types'; import { DefaultLauncher, LauncherFacade, LauncherOptions } from './types'; @@ -22,17 +24,22 @@ export class Launcher extends Observable implements DefaultLauncher { private activeComponents: ComponentNames[] = []; private componentsToAttachAfterJoin: Partial[] = []; private activeComponentsInstances: Partial[] = []; - private participant: Participant; private group: Group; private realtime: AblyRealtimeService; private eventBus: EventBus = new EventBus(); - private participants: Participant[] = []; + private participant: PublicSubject; + private participants: PublicSubject; + constructor({ participant, group }: LauncherOptions) { super(); + const { localParticipant, participants } = useGlobalStore(); + + this.participant = localParticipant; + this.participants = participants; - this.participant = { + this.participant.value = { ...participant, type: ParticipantType.GUEST, }; @@ -69,7 +76,7 @@ export class Launcher extends Observable implements DefaultLauncher { } component.attach({ - localParticipant: this.participant, + localParticipant: this.participant.value, realtime: this.realtime, group: this.group, config: config.configuration, @@ -80,7 +87,12 @@ export class Launcher extends Observable implements DefaultLauncher { this.activeComponentsInstances.push(component); this.realtime.updateMyProperties({ activeComponents: this.activeComponents }); - ApiService.sendActivity(this.participant.id, this.group.id, this.group.name, component.name); + ApiService.sendActivity( + this.participant.value.id, + this.group.id, + this.group.name, + component.name, + ); }; /** @@ -141,6 +153,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.activeComponents = []; this.activeComponentsInstances = []; this.participant = undefined; + useGlobalStore().destroy(); this.eventBus.destroy(); this.eventBus = undefined; @@ -206,7 +219,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.logger.log('launcher service @ startRealtime'); this.realtime.start({ - participant: this.participant, + participant: this.participant.value, apiKey: config.get('apiKey'), roomId: config.get('roomId'), }); @@ -262,17 +275,17 @@ export class Launcher extends Observable implements DefaultLauncher { })); const localParticipant = participantList.find((participant) => { - return participant?.id === this.participant?.id; + return participant?.id === this.participant.value?.id; }); - if (!isEqual(this.participants, participantList)) { - this.participants = participantList; + if (!isEqual(this.participants.value, participantList)) { + this.participants.value = participantList; this.publish(ParticipantEvent.LIST_UPDATED, participantList); this.logger.log('Publishing ParticipantEvent.LIST_UPDATED', participantList); } - if (localParticipant && !isEqual(this.participant, localParticipant)) { + if (localParticipant && !isEqual(this.participant.value, localParticipant)) { this.activeComponents = localParticipant.activeComponents ?? []; this.activeComponentsInstances = this.activeComponentsInstances.filter((component) => { /** @@ -284,7 +297,7 @@ export class Launcher extends Observable implements DefaultLauncher { return this.activeComponents.includes(component.name); }); - this.participant = localParticipant; + this.participant.value = localParticipant; this.publish(ParticipantEvent.LOCAL_UPDATED, localParticipant); this.logger.log('Publishing ParticipantEvent.UPDATED', localParticipant); @@ -305,13 +318,13 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoined = (ablyParticipant: AblyParticipant): void => { this.logger.log('launcher service @ onParticipantJoined'); - const participant = this.participants.find( + const participant = this.participants.value.find( (participant) => participant.id === ablyParticipant.data.id, ); if (!participant) return; - if (participant.id === this.participant.id) { + if (participant.id === this.participant.value.id) { this.logger.log('launcher service @ onParticipantJoined - local participant joined'); this.publish(ParticipantEvent.LOCAL_JOINED, participant); this.attachComponentsAfterJoin(); @@ -330,13 +343,13 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantLeave = (ablyParticipant: AblyParticipant): void => { this.logger.log('launcher service @ onParticipantLeave'); - const participant = this.participants.find((participant) => { - return participant.id === ablyParticipant.data.id; - }); + const participant = this.participants.value.find( + (participant) => participant.id === ablyParticipant.data.id, + ); if (!participant) return; - if (participant.id === this.participant.id) { + if (participant.id === this.participant.value.id) { this.logger.log('launcher service @ onParticipantLeave - local participant left'); this.publish(ParticipantEvent.LOCAL_LEFT, participant); } From 2d30fb0a3dd5014fb90fdcebf2842cdf33c5f3be Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 1 Mar 2024 11:12:13 -0300 Subject: [PATCH 04/83] feat: install rxjs in the project --- package.json | 1 + yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 4df129f3..cdc652c7 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.4.3", + "rxjs": "^7.8.1", "semantic-release-version-file": "^1.0.2" }, "config": { diff --git a/yarn.lock b/yarn.lock index 6e13cc34..50aa91d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9983,6 +9983,13 @@ rxjs@^7.0.0, rxjs@^7.5.5: dependencies: tslib "^2.1.0" +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-array-concat@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" From b4ecd39da6a2bed7165c77295ebc4722ff77776e Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 6 Mar 2024 20:17:09 -0300 Subject: [PATCH 05/83] feat: abstract logic of stores inside base classes --- src/common/types/stores.types.ts | 22 +++ src/common/utils/observer.ts | 2 +- src/common/utils/use-store.test.ts | 13 ++ src/common/utils/use-store.ts | 56 ++++++++ src/components/base/index.test.ts | 22 +-- src/components/base/index.ts | 13 +- src/components/base/types.ts | 9 +- .../comments/canvas-pin-adapter/index.ts | 9 +- .../comments/canvas-pin-adapter/types.ts | 5 - .../comments/html-pin-adapter/index.test.ts | 12 -- .../comments/html-pin-adapter/index.ts | 15 +- .../comments/html-pin-adapter/types.ts | 5 - src/components/comments/index.test.ts | 36 +++-- src/components/comments/index.ts | 24 ++-- src/components/comments/types.ts | 2 +- .../presence-mouse/canvas/index.test.ts | 4 +- src/components/presence-mouse/canvas/index.ts | 7 + .../presence-mouse/html/index.test.ts | 3 +- src/components/presence-mouse/html/index.ts | 7 +- src/components/realtime/index.test.ts | 8 -- src/components/video/index.test.ts | 19 +-- src/components/video/index.ts | 6 + src/components/who-is-online/index.test.ts | 4 +- src/components/who-is-online/index.ts | 32 +++-- src/components/who-is-online/types.ts | 5 +- src/core/launcher/index.test.ts | 22 +-- src/core/launcher/index.ts | 21 +-- src/index.ts | 7 +- src/services/stores/common/types.ts | 4 +- src/services/stores/common/utils.ts | 2 +- src/services/stores/global/index.test.ts | 0 src/services/stores/global/index.ts | 18 ++- src/services/stores/subject/index.test.ts | 129 ++++++++++++++++++ src/services/stores/subject/index.ts | 25 ++-- src/services/stores/who-is-online/index.ts | 34 +++++ src/web-components/base/index.ts | 14 ++ src/web-components/base/types.ts | 3 + .../components/annotation-pin.test.ts | 7 + .../comments/components/annotation-pin.ts | 9 +- .../who-is-online/components/messages.test.ts | 5 + .../who-is-online/components/messages.ts | 13 +- .../who-is-online/components/types.ts | 1 - .../who-is-online/who-is-online.test.ts | 11 +- .../who-is-online/who-is-online.ts | 11 +- 44 files changed, 495 insertions(+), 181 deletions(-) create mode 100644 src/common/types/stores.types.ts create mode 100644 src/common/utils/use-store.test.ts create mode 100644 src/common/utils/use-store.ts delete mode 100644 src/services/stores/global/index.test.ts create mode 100644 src/services/stores/who-is-online/index.ts diff --git a/src/common/types/stores.types.ts b/src/common/types/stores.types.ts new file mode 100644 index 00000000..270bfbe4 --- /dev/null +++ b/src/common/types/stores.types.ts @@ -0,0 +1,22 @@ +import { useGlobalStore } from '../../services/stores'; +import { PublicSubject } from '../../services/stores/common/types'; + +export enum StoreType { + GLOBAL = 'global-store', + COMMENTS = 'comments-store', + WHO_IS_ONLINE = 'who-is-online-store', +} + +type StoreApi any> = { + [K in keyof ReturnType]: { + subscribe(callback?: (value: keyof T) => void): void; + subject: PublicSubject; + publish(value: T): void; + }; +}; + +// When creating new Stores, expand the ternary with the new Store. For example: +// ...T extends StoreType.GLOBAL ? StoreApi : T extends StoreType.WHO_IS_ONLINE ? StoreApi : never; +// Yes, it will be a little bit verbose, but it's not like we'll be creating more and more Stores just for. Rarely will someone need to come here +export type Store = T extends StoreType.GLOBAL ? StoreApi : never; +export type StoresTypes = typeof StoreType; diff --git a/src/common/utils/observer.ts b/src/common/utils/observer.ts index f96c5a6a..cc1155e2 100644 --- a/src/common/utils/observer.ts +++ b/src/common/utils/observer.ts @@ -59,7 +59,7 @@ export class Observer { 'superviz-sdk:observer-helper:publish:error', ` Failed to execute callback on publish value. - Callback: ${callback} + Callback: ${callback.name} Event: ${JSON.stringify(event)} Error: ${error} `, diff --git a/src/common/utils/use-store.test.ts b/src/common/utils/use-store.test.ts new file mode 100644 index 00000000..3d5f3454 --- /dev/null +++ b/src/common/utils/use-store.test.ts @@ -0,0 +1,13 @@ +import { StoreType } from '../types/stores.types'; + +import { useStore } from './use-store'; + +describe('useStore', () => { + test('should return an api to use a store', () => { + const result = useStore.call(this, StoreType.GLOBAL); + + expect(result).toHaveProperty('subscribe'); + expect(result).toHaveProperty('subject'); + expect(result).toHaveProperty('publish'); + }); +}); diff --git a/src/common/utils/use-store.ts b/src/common/utils/use-store.ts new file mode 100644 index 00000000..006d96fa --- /dev/null +++ b/src/common/utils/use-store.ts @@ -0,0 +1,56 @@ +import { PublicSubject } from '../../services/stores/common/types'; +import { useGlobalStore } from '../../services/stores/global'; +import { Store, StoreType, StoresTypes } from '../types/stores.types'; + +const stores = { + [StoreType.GLOBAL]: useGlobalStore, +}; + +/** + * @function subscribe + * @description Subscribes to a subject and either update the value of the property each time there is a change, or call the callback that provides a custom behavior to the subscription + * @param name The name of the property to be updated in case there isn't a callback + * @param subject The subject to be subscribed + * @param callback The callback to be called each time there is a change + */ +function subscribeTo( + name: string, + subject: PublicSubject, + callback?: (value: T) => void, +): void { + subject.subscribe(this, () => { + this[name] = subject.value; + + if (callback) { + callback(subject.value); + } + }); + + this.unsubscribeFrom.push(subject.unsubscribe); +} + +/** + * @function useGlobalStore + * @description Returns a proxy of the global store data and a subscribe function to be used in the components + */ +export function useStore(name: T): Store { + // @TODO - Improve types to get better sugestions when writing code + const storeData = stores[name as StoreType](); + const bindedSubscribeTo = subscribeTo.bind(this); + + const proxy = new Proxy(storeData, { + get(store: Store, valueName: string) { + return { + subscribe>(callback?: (value: K) => void) { + bindedSubscribeTo(valueName, store[valueName], callback); + }, + subject: store[valueName] as typeof storeData, + publish(newValue: keyof Store) { + this.subject.value = newValue; + }, + }; + }, + }); + + return proxy; +} diff --git a/src/components/base/index.test.ts b/src/components/base/index.test.ts index 5821f44a..9c49f03b 100644 --- a/src/components/base/index.test.ts +++ b/src/components/base/index.test.ts @@ -3,11 +3,12 @@ import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; -import { Group, Participant } from '../../common/types/participant.types'; +import { Group } from '../../common/types/participant.types'; import { Logger } from '../../common/utils'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; import { AblyRealtimeService } from '../../services/realtime'; +import { useGlobalStore } from '../../services/stores'; import { ComponentNames } from '../types'; import { BaseComponent } from '.'; @@ -48,6 +49,10 @@ describe('BaseComponent', () => { console.error = jest.fn(); jest.clearAllMocks(); + const { localParticipant, group } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + group.value = MOCK_GROUP; + DummyComponentInstance = new DummyComponent(); }); @@ -59,8 +64,6 @@ describe('BaseComponent', () => { test('should not call start if realtime is not joined room', () => { DummyComponentInstance.attach({ realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -83,8 +86,6 @@ describe('BaseComponent', () => { DummyComponentInstance.attach({ realtime: ablyMock as AblyRealtimeService, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -101,27 +102,22 @@ describe('BaseComponent', () => { expect(DummyComponentInstance.attach).toBeDefined(); DummyComponentInstance.attach({ - localParticipant: MOCK_LOCAL_PARTICIPANT, realtime: REALTIME_MOCK, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); - expect(DummyComponentInstance['localParticipant']).toEqual(MOCK_LOCAL_PARTICIPANT); expect(DummyComponentInstance['realtime']).toEqual(REALTIME_MOCK); expect(DummyComponentInstance['isAttached']).toBeTruthy(); expect(DummyComponentInstance['start']).toBeCalled(); }); - test('should throw error if realtime or localParticipant are not provided', () => { + test('should throw error if realtime is not provided', () => { expect(DummyComponentInstance.attach).toBeDefined(); expect(() => { DummyComponentInstance.attach({ - localParticipant: null as unknown as Participant, realtime: null as unknown as AblyRealtimeService, - group: null as unknown as Group, config: null as unknown as Configuration, eventBus: null as unknown as EventBus, }); @@ -135,9 +131,7 @@ describe('BaseComponent', () => { expect(DummyComponentInstance.detach).toBeDefined(); DummyComponentInstance.attach({ - localParticipant: MOCK_LOCAL_PARTICIPANT, realtime: REALTIME_MOCK, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -156,9 +150,7 @@ describe('BaseComponent', () => { DummyComponentInstance['destroy'] = jest.fn(); DummyComponentInstance.attach({ - localParticipant: MOCK_LOCAL_PARTICIPANT, realtime: REALTIME_MOCK, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 7a1d750f..30a17bdb 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -1,6 +1,8 @@ import { ComponentLifeCycleEvent } from '../../common/types/events.types'; -import { Group, Participant } from '../../common/types/participant.types'; +import { Group } from '../../common/types/participant.types'; +import { StoreType } from '../../common/types/stores.types'; import { Logger, Observable } from '../../common/utils'; +import { useStore } from '../../common/utils/use-store'; import config from '../../services/config'; import { EventBus } from '../../services/event-bus'; import { AblyRealtimeService } from '../../services/realtime'; @@ -11,12 +13,12 @@ import { DefaultAttachComponentOptions } from './types'; export abstract class BaseComponent extends Observable { public abstract name: ComponentNames; protected abstract logger: Logger; - protected localParticipant: Participant; protected group: Group; protected realtime: AblyRealtimeService; protected eventBus: EventBus; - protected isAttached = false; + protected unsubscribeFrom: Array<(id: unknown) => void> = []; + protected useStore = useStore.bind(this) as typeof useStore; /** * @function attach @@ -32,7 +34,7 @@ export abstract class BaseComponent extends Observable { throw new Error(message); } - const { realtime, localParticipant, group, config: globalConfig, eventBus } = params; + const { realtime, config: globalConfig, eventBus } = params; if (!realtime.isDomainWhitelisted) { const message = `Component ${this.name} can't be used because this website's domain is not whitelisted. Please add your domain in https://dashboard.superviz.com/developer`; @@ -43,8 +45,6 @@ export abstract class BaseComponent extends Observable { config.setConfig(globalConfig); this.realtime = realtime; - this.localParticipant = localParticipant; - this.group = group; this.eventBus = eventBus; this.isAttached = true; @@ -87,7 +87,6 @@ export abstract class BaseComponent extends Observable { this.observers = undefined; this.realtime = undefined; - this.localParticipant = undefined; this.isAttached = false; }; diff --git a/src/components/base/types.ts b/src/components/base/types.ts index 59bfb861..5ffb295f 100644 --- a/src/components/base/types.ts +++ b/src/components/base/types.ts @@ -2,11 +2,16 @@ import { Group, Participant } from '../../common/types/participant.types'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; import { AblyRealtimeService } from '../../services/realtime'; +import { useGlobalStore } from '../../services/stores'; export interface DefaultAttachComponentOptions { realtime: AblyRealtimeService; - localParticipant: Participant; - group: Group; config: Configuration; eventBus: EventBus; } + +export type GlobalStore = { + [K in keyof ReturnType]: { + subscribe(callback?: (value: unknown) => void): void; + }; +}; diff --git a/src/components/comments/canvas-pin-adapter/index.ts b/src/components/comments/canvas-pin-adapter/index.ts index dbdf5bae..45d4b798 100644 --- a/src/components/comments/canvas-pin-adapter/index.ts +++ b/src/components/comments/canvas-pin-adapter/index.ts @@ -3,7 +3,7 @@ import { Logger, Observer } from '../../../common/utils'; import { PinMode } from '../../../web-components/comments/components/types'; import { Annotation, PinAdapter, PinCoordinates } from '../types'; -import { CanvasSides, SimpleParticipant } from './types'; +import { CanvasSides } from './types'; export class CanvasPin implements PinAdapter { private logger: Logger; @@ -22,7 +22,6 @@ export class CanvasPin implements PinAdapter { private temporaryPinCoordinates: { x: number; y: number } | null = null; private commentsSide: 'left' | 'right' = 'left'; private movedTemporaryPin: boolean; - private localParticipant: SimpleParticipant = {}; private originalCanvasCursor: string; declare participants: ParticipantByGroupApi[]; @@ -164,9 +163,7 @@ export class CanvasPin implements PinAdapter { temporaryPin.setAttribute('commentsSide', this.commentsSide); temporaryPin.setAttribute('position', JSON.stringify(this.temporaryPinCoordinates)); temporaryPin.setAttribute('annotation', JSON.stringify({})); - temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); temporaryPin.setAttribute('participantsList', JSON.stringify(this.participants)); - temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); temporaryPin.setAttributeNode(document.createAttribute('active')); this.divWrapper.appendChild(temporaryPin); } @@ -201,10 +198,8 @@ export class CanvasPin implements PinAdapter { document.body.addEventListener('click', this.hideTemporaryPin); } - public setCommentsMetadata = (side: 'left' | 'right', avatar: string, name: string): void => { + public setCommentsMetadata = (side: 'left' | 'right'): void => { this.commentsSide = side; - this.localParticipant.avatar = avatar; - this.localParticipant.name = name; }; /** diff --git a/src/components/comments/canvas-pin-adapter/types.ts b/src/components/comments/canvas-pin-adapter/types.ts index b0b5e849..4f56e5e6 100644 --- a/src/components/comments/canvas-pin-adapter/types.ts +++ b/src/components/comments/canvas-pin-adapter/types.ts @@ -8,8 +8,3 @@ export interface CanvasSides { right: number; bottom: number; } - -export interface SimpleParticipant { - name?: string; - avatar?: string; -} diff --git a/src/components/comments/html-pin-adapter/index.test.ts b/src/components/comments/html-pin-adapter/index.test.ts index 4e2e968c..4aeda658 100644 --- a/src/components/comments/html-pin-adapter/index.test.ts +++ b/src/components/comments/html-pin-adapter/index.test.ts @@ -487,18 +487,6 @@ describe('HTMLPinAdapter', () => { }); }); - describe('setCommentsMetadata', () => { - test('should store updated data about comments and local participant', () => { - instance.setCommentsMetadata('right', 'user-avatar', 'user name'); - - expect(instance['commentsSide']).toEqual('right'); - expect(instance['localParticipant']).toEqual({ - avatar: 'user-avatar', - name: 'user name', - }); - }); - }); - describe('resetPins', () => { test('should remove active on Escape key', () => { instance.updateAnnotations([MOCK_ANNOTATION_HTML]); diff --git a/src/components/comments/html-pin-adapter/index.ts b/src/components/comments/html-pin-adapter/index.ts index 3246bade..44de9750 100644 --- a/src/components/comments/html-pin-adapter/index.ts +++ b/src/components/comments/html-pin-adapter/index.ts @@ -5,13 +5,7 @@ import { Logger, Observer } from '../../../common/utils'; import { PinMode } from '../../../web-components/comments/components/types'; import { Annotation, PinAdapter, PinCoordinates } from '../types'; -import { - HorizontalSide, - Simple2DPoint, - SimpleParticipant, - TemporaryPinData, - HTMLPinOptions, -} from './types'; +import { HorizontalSide, Simple2DPoint, TemporaryPinData, HTMLPinOptions } from './types'; export class HTMLPin implements PinAdapter { // Public properties @@ -21,7 +15,6 @@ export class HTMLPin implements PinAdapter { // Private properties // Comments data private annotations: Annotation[]; - private localParticipant: SimpleParticipant = {}; declare participants: ParticipantByGroupApi[]; @@ -339,10 +332,8 @@ export class HTMLPin implements PinAdapter { * @param {string} name the name of the local participant * @returns {void} */ - public setCommentsMetadata = (side: HorizontalSide, avatar: string, name: string): void => { + public setCommentsMetadata = (side: HorizontalSide): void => { this.commentsSide = side; - this.localParticipant.avatar = avatar; - this.localParticipant.name = name; }; /** @@ -415,8 +406,6 @@ export class HTMLPin implements PinAdapter { temporaryPin.setAttribute('commentsSide', this.commentsSide); temporaryPin.setAttribute('position', JSON.stringify(this.temporaryPinCoordinates)); temporaryPin.setAttribute('annotation', JSON.stringify({})); - temporaryPin.setAttribute('localAvatar', this.localParticipant.avatar ?? ''); - temporaryPin.setAttribute('localName', this.localParticipant.name ?? ''); temporaryPin.setAttribute('participantsList', JSON.stringify(this.participants)); temporaryPin.setAttribute('keepPositionRatio', ''); diff --git a/src/components/comments/html-pin-adapter/types.ts b/src/components/comments/html-pin-adapter/types.ts index 70694d3d..c24ca33c 100644 --- a/src/components/comments/html-pin-adapter/types.ts +++ b/src/components/comments/html-pin-adapter/types.ts @@ -1,8 +1,3 @@ -export interface SimpleParticipant { - name?: string; - avatar?: string; -} - export interface Simple2DPoint { x: number; y: number; diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index 2c0b2efc..29388eda 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -1,3 +1,5 @@ +import { TestScheduler } from 'rxjs/testing'; + import { MOCK_ANNOTATION } from '../../../__mocks__/comments.mock'; import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; @@ -8,10 +10,11 @@ import { CommentEvent } from '../../common/types/events.types'; import { ParticipantByGroupApi } from '../../common/types/participant.types'; import sleep from '../../common/utils/sleep'; import ApiService from '../../services/api'; +import { useGlobalStore } from '../../services/stores'; import { CommentsFloatButton } from '../../web-components'; import { ComponentNames } from '../types'; -import { PinAdapter, CommentsSide, Annotation, PinCoordinates } from "./types"; +import { PinAdapter, CommentsSide, Annotation, PinCoordinates } from './types'; import { Comments } from './index'; @@ -46,7 +49,10 @@ jest.mock('../../services/api', () => ({ resolveAnnotation: jest.fn().mockImplementation(() => []), deleteComment: jest.fn().mockImplementation(() => []), deleteAnnotation: jest.fn().mockImplementation(() => []), - fetchParticipantsByGroup: jest.fn().mockImplementation((): ParticipantByGroupApi[] => MOCK_PARTICIPANTS),})); + fetchParticipantsByGroup: jest + .fn() + .mockImplementation((): ParticipantByGroupApi[] => MOCK_PARTICIPANTS), +})); const DummiePinAdapter: PinAdapter = { destroy: jest.fn(), @@ -65,12 +71,14 @@ describe('Comments', () => { beforeEach(() => { jest.clearAllMocks(); + const { localParticipant, group } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + group.value = MOCK_GROUP; + commentsComponent = new Comments(DummiePinAdapter); commentsComponent.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -323,8 +331,6 @@ describe('Comments', () => { commentsComponent.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -342,8 +348,6 @@ describe('Comments', () => { commentsComponent.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -741,7 +745,9 @@ describe('Comments', () => { commentsComponent['element'].participantsListed = jest.fn(); await commentsComponent['element'].participantsListed(MOCK_PARTICIPANTS); - expect(commentsComponent['element'].participantsListed).toHaveBeenCalledWith(MOCK_PARTICIPANTS); + expect(commentsComponent['element'].participantsListed).toHaveBeenCalledWith( + MOCK_PARTICIPANTS, + ); }); }); @@ -749,15 +755,17 @@ describe('Comments', () => { test('should create a mention', async () => { const response = await ApiService.createMentions({ commentsId: 'any_comment_id', - participants: [{ - id: 'any_mention_userId', - readed: 0, - }] + participants: [ + { + id: 'any_mention_userId', + readed: 0, + }, + ], }); expect(response).toEqual([]); }); - }) + }); describe('openThreads', () => { afterEach(() => { diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts index 89eae06b..cbb138ff 100644 --- a/src/components/comments/index.ts +++ b/src/components/comments/index.ts @@ -1,8 +1,10 @@ import { CommentEvent } from '../../common/types/events.types'; -import { ParticipantByGroupApi } from '../../common/types/participant.types'; +import { Participant, ParticipantByGroupApi } from '../../common/types/participant.types'; +import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; import ApiService from '../../services/api'; import config from '../../services/config'; +import subject from '../../services/stores/subject'; import type { Comments as CommentElement } from '../../web-components'; import { CommentsFloatButton } from '../../web-components/comments/components/float-button'; import { BaseComponent } from '../base'; @@ -31,6 +33,7 @@ export class Comments extends BaseComponent { private coordinates: AnnotationPositionInfo; private hideDefaultButton: boolean; private pinActive: boolean; + private localParticipantId: string; constructor(pinAdapter: PinAdapter, options?: CommentsOptions) { super(); @@ -47,14 +50,17 @@ export class Comments extends BaseComponent { this.setStyles(options?.styles); setTimeout(() => { - pinAdapter.setCommentsMetadata( - this.layoutOptions?.position ?? 'left', - this.localParticipant?.avatar?.imageUrl, - this.localParticipant?.name, - ); + pinAdapter.setCommentsMetadata(this.layoutOptions?.position ?? 'left'); }); this.pinAdapter = pinAdapter; + + const { group, localParticipant } = this.useStore(StoreType.GLOBAL); + group.subscribe(); + + localParticipant.subscribe((participant: Participant) => { + this.localParticipantId = participant.id; + }); } /** @@ -396,7 +402,7 @@ export class Comments extends BaseComponent { roomId: config.get('roomId'), position: JSON.stringify(position), url, - userId: this.localParticipant.id, + userId: this.localParticipantId, }, ); @@ -465,7 +471,7 @@ export class Comments extends BaseComponent { config.get('apiKey'), { annotationId, - userId: this.localParticipant.id, + userId: this.localParticipantId, text, }, ); @@ -723,7 +729,7 @@ export class Comments extends BaseComponent { private onAnnotationListUpdate = (message): void => { const { data, clientId } = message; - if (this.localParticipant.id === clientId) return; + if (this.localParticipantId === clientId) return; this.annotations = data; this.element.updateAnnotations(this.annotations); diff --git a/src/components/comments/types.ts b/src/components/comments/types.ts index 831941d8..f75ca4ab 100644 --- a/src/components/comments/types.ts +++ b/src/components/comments/types.ts @@ -36,7 +36,7 @@ export interface PinAdapter { updateAnnotations(annotations: Annotation[]): void; removeAnnotationPin(uuid: string): void; onPinFixedObserver: Observer; - setCommentsMetadata(side: 'left' | 'right', localUserAvatar: string, name: string): void; + setCommentsMetadata(side: 'left' | 'right'): void; participantsList: ParticipantByGroupApi[]; } diff --git a/src/components/presence-mouse/canvas/index.test.ts b/src/components/presence-mouse/canvas/index.test.ts index d30efa3f..1145f6b7 100644 --- a/src/components/presence-mouse/canvas/index.test.ts +++ b/src/components/presence-mouse/canvas/index.test.ts @@ -33,8 +33,6 @@ const createMousePointers = (): PointersCanvas => { presenceMouseComponent.attach({ realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -44,6 +42,8 @@ const createMousePointers = (): PointersCanvas => { ...MOCK_CANVAS, } as unknown as HTMLCanvasElement; + presenceMouseComponent['localParticipant'] = MOCK_LOCAL_PARTICIPANT; + return presenceMouseComponent; }; diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 7b220cad..d25e394f 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -1,5 +1,8 @@ import { RealtimeEvent } from '../../../common/types/events.types'; +import { Participant } from '../../../common/types/participant.types'; +import { StoreType } from '../../../common/types/stores.types'; import { Logger } from '../../../common/utils'; +import { useGlobalStore } from '../../../services/stores'; import { BaseComponent } from '../../base'; import { ComponentNames } from '../../types'; import { ParticipantMouse, PresenceMouseProps } from '../types'; @@ -14,6 +17,7 @@ export class PointersCanvas extends BaseComponent { private goToMouseCallback: PresenceMouseProps['onGoToPresence']; private following: string; private isPrivate: boolean; + private localParticipant: Participant; constructor(canvasId: string, options?: PresenceMouseProps) { super(); @@ -32,6 +36,9 @@ export class PointersCanvas extends BaseComponent { this.animateFrame = requestAnimationFrame(this.animate); this.goToMouseCallback = options?.onGoToPresence; + + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe(); } private get textColorValues(): number[] { diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index c477d977..ff7025d4 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -9,11 +9,10 @@ import { PointersHTML } from '.'; const createMousePointers = (): PointersHTML => { const presenceMouseComponent = new PointersHTML('html'); + presenceMouseComponent['localParticipant'] = MOCK_LOCAL_PARTICIPANT; presenceMouseComponent.attach({ realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index fadd3430..0ceb2f84 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -2,6 +2,8 @@ import { isEqual } from 'lodash'; import { RealtimeEvent } from '../../../common/types/events.types'; import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; +import { Participant } from '../../../common/types/participant.types'; +import { StoreType } from '../../../common/types/stores.types'; import { Logger } from '../../../common/utils'; import { BaseComponent } from '../../base'; import { ComponentNames } from '../../types'; @@ -13,6 +15,7 @@ export class PointersHTML extends BaseComponent { // Realtime data private presences: Map = new Map(); + private localParticipant: Participant; // Elements private container: HTMLElement; @@ -74,6 +77,8 @@ export class PointersHTML extends BaseComponent { } this.name = ComponentNames.PRESENCE; + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe(); this.goToPresenceCallback = options?.onGoToPresence; this.dataAttributeName = options?.dataAttributeName || this.dataAttributeName; @@ -661,7 +666,7 @@ export class PointersHTML extends BaseComponent { } /** - * @function updateVoidElementWrapper + * @function updateSVGElementWrapper * @description - Updates the position of each wrapper for void elements * @returns {void} */ diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index 9c2c5870..916678c8 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -29,8 +29,6 @@ describe('realtime component', () => { RealtimeComponentInstance = new Realtime(); RealtimeComponentInstance.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -76,8 +74,6 @@ describe('realtime component', () => { RealtimeComponentInstance.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -96,8 +92,6 @@ describe('realtime component', () => { RealtimeComponentInstance.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -122,8 +116,6 @@ describe('realtime component', () => { RealtimeComponentInstance.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index cf48888f..c2900cdd 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -1,11 +1,7 @@ import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; -import { - MOCK_AVATAR, - MOCK_GROUP, - MOCK_LOCAL_PARTICIPANT, -} from '../../../__mocks__/participants.mock'; +import { MOCK_AVATAR, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { DeviceEvent, @@ -96,10 +92,9 @@ describe('VideoConference', () => { allowGuests: false, }); + VideoConferenceInstance['localParticipant'] = MOCK_LOCAL_PARTICIPANT; VideoConferenceInstance.attach({ realtime: MOCK_REALTIME, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -116,13 +111,13 @@ describe('VideoConference', () => { defaultAvatars: true, }); + VideoConferenceInstance['localParticipant'] = { + ...MOCK_LOCAL_PARTICIPANT, + avatar: MOCK_AVATAR, + }; + VideoConferenceInstance.attach({ realtime: MOCK_REALTIME, - localParticipant: { - ...MOCK_LOCAL_PARTICIPANT, - avatar: MOCK_AVATAR, - }, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); diff --git a/src/components/video/index.ts b/src/components/video/index.ts index a1057e17..723a4c6d 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -13,6 +13,7 @@ import { TranscriptState, } from '../../common/types/events.types'; import { Participant, ParticipantType } from '../../common/types/participant.types'; +import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; import { BrowserService } from '../../services/browser'; import config from '../../services/config'; @@ -40,6 +41,7 @@ export class VideoConference extends BaseComponent { protected logger: Logger; private participantToFrameList: ParticipandToFrame[] = []; private participantsOnMeeting: Partial[] = []; + private localParticipant: Participant; private videoManager: VideoConfereceManager; private connectionService: ConnectionService; @@ -59,6 +61,10 @@ export class VideoConference extends BaseComponent { }; this.name = ComponentNames.VIDEO_CONFERENCE; + const { localParticipant, group } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe(); + group.subscribe(); + this.logger = new Logger(`@superviz/sdk/${ComponentNames.VIDEO_CONFERENCE}`); this.browserService = new BrowserService(); diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index fcd9daca..94834911 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -10,6 +10,7 @@ import { import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; +import { useGlobalStore } from '../../services/stores'; import { WhoIsOnline } from './index'; @@ -22,12 +23,11 @@ describe('Who Is Online', () => { whoIsOnlineComponent = new WhoIsOnline(); whoIsOnlineComponent.attach({ realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); + whoIsOnlineComponent['localParticipantId'] = MOCK_LOCAL_PARTICIPANT.id; whoIsOnlineComponent['element'].updateParticipants = jest.fn(); const gray = MeetingColorsHex[16]; diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index a911e691..2e036557 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -1,6 +1,7 @@ import { isEqual } from 'lodash'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; +import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; import { AblyParticipant } from '../../services/realtime/ably/types'; import { WhoIsOnline as WhoIsOnlineElement } from '../../web-components'; @@ -16,6 +17,7 @@ export class WhoIsOnline extends BaseComponent { private position: WhoIsOnlinePosition; private participants: Participant[] = []; private following: string; + private localParticipantId: string; constructor(options?: WhoIsOnlinePosition | WhoIsOnlineOptions) { super(); @@ -38,10 +40,16 @@ export class WhoIsOnline extends BaseComponent { * @returns {void} */ protected start(): void { + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe((value: Participant) => { + this.localParticipantId = value.id; + }); + this.subscribeToRealtimeEvents(); this.positionWhoIsOnline(); this.addListeners(); - this.realtime.enterWIOChannel(this.localParticipant); + + this.realtime.enterWIOChannel(localParticipant.subject.value as Participant); } /** @@ -133,14 +141,14 @@ export class WhoIsOnline extends BaseComponent { const participants = updatedParticipants .filter(({ data: { isPrivate, id } }) => { - return !isPrivate || (isPrivate && id === this.localParticipant.id); + return !isPrivate || (isPrivate && id === this.localParticipantId); }) .map(({ data }) => { const { slotIndex, id, name, avatar, activeComponents } = data as Participant; const { color } = this.realtime.getSlotColor(slotIndex); - const isLocal = this.localParticipant.id === id; + const isLocal = this.localParticipantId === id; const joinedPresence = activeComponents.some((component) => component.includes('presence')); - this.setLocalData(isLocal, !joinedPresence, color, joinedPresence); + this.setLocalData(isLocal, !joinedPresence, joinedPresence); return { name, id, slotIndex, color, isLocal, joinedPresence, avatar }; }); @@ -156,16 +164,14 @@ export class WhoIsOnline extends BaseComponent { this.element.updateParticipants(this.participants); }; - private setLocalData = ( - local: boolean, - disable: boolean, - color: string, - joinedPresence: boolean, - ) => { + private setLocalData = (local: boolean, disable: boolean, joinedPresence: boolean) => { if (!local) return; this.element.disableDropdown = disable; - this.element.localParticipantData = { color, id: this.localParticipant.id, joinedPresence }; + this.element.localParticipantData = { + ...this.element.localParticipantData, + joinedPresence, + }; }; /** @@ -220,7 +226,7 @@ export class WhoIsOnline extends BaseComponent { * @returns {void} */ private goToMousePointer = ({ detail: { id } }: CustomEvent) => { - if (id === this.localParticipant.id) return; + if (id === this.localParticipantId) return; this.eventBus.publish(RealtimeEvent.REALTIME_GO_TO_PARTICIPANT, id); this.publish(WhoIsOnlineEvent.GO_TO_PARTICIPANT, id); @@ -263,7 +269,7 @@ export class WhoIsOnline extends BaseComponent { }; private setFollow = (following) => { - if (following.clientId === this.localParticipant.id) return; + if (following.clientId === this.localParticipantId) return; this.followMousePointer({ detail: { id: following?.data?.id } } as CustomEvent); diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 9d72991c..419b59ca 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -1,5 +1,4 @@ -import { Avatar, Participant as GeneralParticipant } from '../../common/types/participant.types'; -import { ComponentNames } from '../types'; +import { Participant as GeneralParticipant } from '../../common/types/participant.types'; export enum Position { TOP_LEFT = 'top-left', @@ -9,7 +8,7 @@ export enum Position { } export interface Participant extends GeneralParticipant { - slotIndex: number; + slotIndex?: number; isLocal?: boolean; joinedPresence?: boolean; isPrivate?: boolean; diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 70016742..084f75cf 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -3,11 +3,11 @@ import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types'; -import { ParticipantType } from '../../common/types/participant.types'; import { BaseComponent } from '../../components/base'; import { ComponentNames } from '../../components/types'; import LimitsService from '../../services/limits'; import { AblyParticipant } from '../../services/realtime/ably/types'; +import { useGlobalStore } from '../../services/stores'; import { LauncherFacade, LauncherOptions } from './types'; @@ -27,9 +27,7 @@ jest.mock('../../services/api'); const MOCK_COMPONENT = { name: ComponentNames.VIDEO_CONFERENCE, - attach: jest.fn(() => { - console.log('attach'); - }), + attach: jest.fn(), detach: jest.fn(), } as unknown as BaseComponent; @@ -44,11 +42,14 @@ describe('Launcher', () => { beforeEach(() => { console.warn = jest.fn(); console.error = jest.fn(); - console.log = jest.fn(); + console.log = jest.spyOn(console, 'log').mockImplementation(console.log) as any; jest.clearAllMocks(); jest.restoreAllMocks(); + const { localParticipant } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + LauncherInstance = new Launcher(DEFAULT_INITIALIZATION_MOCK); }); @@ -56,7 +57,7 @@ describe('Launcher', () => { expect(Launcher).toBeDefined(); }); - test('should be inicialize realtime service', () => { + test('should initialize realtime service', () => { expect(ABLY_REALTIME_MOCK.start).toHaveBeenCalled(); }); @@ -86,15 +87,13 @@ describe('Launcher', () => { expect(spy).toHaveBeenCalledWith(MOCK_COMPONENT); }); - test('should be add component', () => { + test('should add component', () => { LimitsService.checkComponentLimit = jest.fn().mockReturnValue(true); LauncherInstance.addComponent(MOCK_COMPONENT); expect(MOCK_COMPONENT.attach).toHaveBeenCalledWith({ - localParticipant: { ...MOCK_LOCAL_PARTICIPANT, type: ParticipantType.GUEST }, realtime: ABLY_REALTIME_MOCK, - group: MOCK_GROUP, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, }); @@ -242,6 +241,7 @@ describe('Launcher', () => { id: 'unit-test-participant-ably-id', timestamp: new Date().getTime(), data: { + id: 'participant1', participantId: 'participant1', }, }; @@ -322,8 +322,8 @@ describe('Launcher', () => { id: 'unit-test-participant-ably-id', timestamp: new Date().getTime(), data: { - id: MOCK_LOCAL_PARTICIPANT.id, - participantId: MOCK_LOCAL_PARTICIPANT.id, + id: 'participant1', + participantId: 'participant1', }, }; diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 1b7bfef2..9ad55e8d 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -24,27 +24,31 @@ export class Launcher extends Observable implements DefaultLauncher { private activeComponents: ComponentNames[] = []; private componentsToAttachAfterJoin: Partial[] = []; private activeComponentsInstances: Partial[] = []; - private group: Group; private realtime: AblyRealtimeService; private eventBus: EventBus = new EventBus(); private participant: PublicSubject; private participants: PublicSubject; + private group: PublicSubject; - constructor({ participant, group }: LauncherOptions) { + constructor({ participant, group: participantGroup }: LauncherOptions) { super(); - const { localParticipant, participants } = useGlobalStore(); + const { localParticipant, participants, group } = useGlobalStore(); this.participant = localParticipant; this.participants = participants; + this.group = group; + + setTimeout(() => { + localParticipant.value = { ...localParticipant.value, color: 'red' }; + }, 20000); this.participant.value = { ...participant, type: ParticipantType.GUEST, }; - - this.group = group; + this.group.value = participantGroup; this.logger = new Logger('@superviz/sdk/launcher'); this.realtime = new AblyRealtimeService( @@ -76,9 +80,7 @@ export class Launcher extends Observable implements DefaultLauncher { } component.attach({ - localParticipant: this.participant.value, realtime: this.realtime, - group: this.group, config: config.configuration, eventBus: this.eventBus, }); @@ -89,8 +91,8 @@ export class Launcher extends Observable implements DefaultLauncher { ApiService.sendActivity( this.participant.value.id, - this.group.id, - this.group.name, + this.group.value.id, + this.group.value.name, component.name, ); }; @@ -342,7 +344,6 @@ export class Launcher extends Observable implements DefaultLauncher { */ private onParticipantLeave = (ablyParticipant: AblyParticipant): void => { this.logger.log('launcher service @ onParticipantLeave'); - const participant = this.participants.value.find( (participant) => participant.id === ablyParticipant.data.id, ); diff --git a/src/index.ts b/src/index.ts index a8e7b191..0dbe81f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,6 @@ export { Participant, Group, Avatar } from './common/types/participant.types'; export { SuperVizSdkOptions, DevicesOptions } from './common/types/sdk-options.types'; export { BrowserService } from './services/browser'; export { BrowserStats } from './services/browser/types'; - export { RealtimeMessage } from './services/realtime/ably/types'; export { LauncherFacade } from './core/launcher/types'; export { Observer } from './common/utils/observer'; @@ -97,6 +96,12 @@ export { CommentEvent, ComponentLifeCycleEvent, WhoIsOnlineEvent, + Comments, + CanvasPin, + HTMLPin, + MousePointers, + WhoIsOnline, + VideoConference, }; export default init; diff --git a/src/services/stores/common/types.ts b/src/services/stores/common/types.ts index ffc5eea0..9af80afc 100644 --- a/src/services/stores/common/types.ts +++ b/src/services/stores/common/types.ts @@ -15,6 +15,6 @@ export type Singleton = { export type PublicSubject = { get value(): T; set value(T); - subscribe: callback>; - unsubscribe: callback; + subscribe: callback>; + unsubscribe: callback; }; diff --git a/src/services/stores/common/utils.ts b/src/services/stores/common/utils.ts index cd7d6277..c5815777 100644 --- a/src/services/stores/common/utils.ts +++ b/src/services/stores/common/utils.ts @@ -4,7 +4,7 @@ export function CreateSingleton(): Singleton { return { set value(value: T) { this.instance = value; - Object.freeze(this); + setTimeout(() => Object.freeze(this)); }, get value() { return this.instance; diff --git a/src/services/stores/global/index.test.ts b/src/services/stores/global/index.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/services/stores/global/index.ts b/src/services/stores/global/index.ts index 6fe80843..e8298089 100644 --- a/src/services/stores/global/index.ts +++ b/src/services/stores/global/index.ts @@ -1,13 +1,14 @@ -import { Participant } from '../../../common/types/participant.types'; -import { Singleton } from '../common/types'; +import { Group, Participant } from '../../../common/types/participant.types'; +import { PublicSubject, Singleton } from '../common/types'; import { CreateSingleton } from '../common/utils'; import subject from '../subject'; const instance: Singleton = CreateSingleton(); -class GlobalStore { - public localParticipant = subject(null); +export class GlobalStore { + public localParticipant = subject(null, true); public participants = subject([]); + public group = subject(null); constructor() { if (instance.value) { @@ -24,14 +25,19 @@ class GlobalStore { } const store = new GlobalStore(); -const localParticipant = store.localParticipant.expose(); -const participants = store.participants.expose(); const destroy = store.destroy.bind(store); +const group = store.group.expose(); +const participants = store.participants.expose(); +const localParticipant = store.localParticipant.expose(); + export function useGlobalStore() { return { localParticipant, participants, destroy, + group, }; } + +export type GlobalStoreReturnType = ReturnType; diff --git a/src/services/stores/subject/index.test.ts b/src/services/stores/subject/index.test.ts index e69de29b..db09bfec 100644 --- a/src/services/stores/subject/index.test.ts +++ b/src/services/stores/subject/index.test.ts @@ -0,0 +1,129 @@ +import subject, { Subject } from '.'; + +const testValues = { + str1: 'string-value-1', + str2: 'string-value-2', + num: 0, + obj: { + property: 'object-value', + }, + id: 'test-id', +}; + +class TestStore { + public property = subject(testValues.str1); +} + +const testStore = new TestStore(); +const { value } = testStore.property.expose(); + +describe('base subject for all stores', () => { + let instance: Subject; + + beforeEach(() => { + instance = subject(testValues.str1); + }); + + describe('should accept multiple types', () => { + test('should instantiate a string subject', () => { + const instance = subject(testValues.str1); + expect(instance.state).toBe(testValues.str1); + expect(typeof instance.state).toBe('string'); + }); + + test('should instantiate a number subject', () => { + const instance = subject(testValues.num); + expect(instance.state).toBe(testValues.num); + expect(typeof instance.state).toBe('number'); + }); + + test('should instantiate an object subject', () => { + const instance = subject(testValues.obj); + expect(instance.state).toBe(testValues.obj); + expect(typeof instance.state).toBe('object'); + }); + }); + + describe('getters & setters', () => { + test('should get the current value', () => { + instance = subject(testValues.str1); + expect(instance['getValue']()).toBe(testValues.str1); + }); + + test('should set a new value', () => { + instance = subject(testValues.str1); + instance['subject'].next = jest.fn(); + instance['setValue'](testValues.str2); + + expect(instance.state).toBe(testValues.str2); + expect(instance['subject'].next).toHaveBeenCalledWith(testValues.str2); + }); + }); + + describe('subscribe & unsubscribe', () => { + test('should expand subscriptions list and subscribe to subject', () => { + instance = subject(testValues.str1); + expect(instance['subscriptions'].size).toBe(0); + + const callback = jest.fn(); + instance['subject'].subscribe = jest.fn().mockImplementation(instance['subject'].subscribe); + + instance['subscribe'](testValues.id, callback); + + expect(instance['subscriptions'].size).toBe(1); + expect(instance['subscriptions'].get(testValues.id)).toBeDefined(); + expect(instance['subject'].subscribe).toHaveBeenCalledWith(callback); + }); + + test('should unsubscribe a subscription', () => { + instance = subject(testValues.str1); + + const callback = jest.fn(); + const unsubscribe = jest.fn(); + + instance['subscribe'](testValues.id, callback); + instance['subscriptions'].get(testValues.id)!.unsubscribe = unsubscribe; + + instance['unsubscribe'](testValues.id); + + expect(instance['subscriptions'].size).toBe(0); + expect(instance['subscriptions'].get(testValues.id)).toBeUndefined(); + expect(unsubscribe).toHaveBeenCalled(); + }); + }); + + describe('destroy', () => { + test('should unsubscribe all subscriptions', () => { + instance = subject(testValues.str1); + + const callback = jest.fn(); + const unsubscribe = jest.fn(); + + instance['subscribe'](testValues.id, callback); + instance['subscriptions'].get(testValues.id)!.unsubscribe = unsubscribe; + + instance['destroy'](); + + expect(instance['subscriptions'].size).toBe(0); + expect(instance['subscriptions'].get(testValues.id)).toBeUndefined(); + expect(unsubscribe).toHaveBeenCalled(); + }); + }); + + describe('expose', () => { + test('should return an object with the public API', () => { + instance = subject(testValues.str1); + + const publicSubject = instance['expose'](); + + expect(publicSubject.value).toBe(testValues.str1); + expect(typeof publicSubject.value).toBe('string'); + expect(publicSubject.subscribe).toBeInstanceOf(Function); + expect(publicSubject.unsubscribe).toBeInstanceOf(Function); + }); + }); + + describe('reactivity', () => { + test('should update the value when the subject is updated', () => {}); + }); +}); diff --git a/src/services/stores/subject/index.ts b/src/services/stores/subject/index.ts index 207a9b16..a73d47ab 100644 --- a/src/services/stores/subject/index.ts +++ b/src/services/stores/subject/index.ts @@ -6,10 +6,12 @@ import { PublicSubject } from '../common/types'; export class Subject { public state: T; private subject: BehaviorSubject; - private subscriptions: Map = new Map(); + private subscriptions: Map = new Map(); + private showLog: boolean; - constructor(state: T, _subject: BehaviorSubject) { + constructor(state: T, _subject: BehaviorSubject, showLog?: boolean) { this.state = state; + this.showLog = !!showLog; this.subject = _subject.pipe( distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }), @@ -20,15 +22,22 @@ export class Subject { return this.state; } - private setValue(newValue: T): void { + private ids: any[] = []; + + private num = Math.floor(Math.random() * 100); + private setValue = (newValue: T): void => { this.state = newValue; this.subject.next(this.state); - } + }; - public subscribe(subscriptionId: string, callback: (value: T) => void) { + private counter = 0; + + public subscribe = (subscriptionId: string | this, callback: (value: T) => void) => { + this.ids.push(subscriptionId); + const number = Math.floor(Math.random() * 100); const subscription = this.subject.subscribe(callback); this.subscriptions.set(subscriptionId, subscription); - } + }; public unsubscribe(subscriptionId: string) { this.subscriptions.get(subscriptionId)?.unsubscribe(); @@ -59,8 +68,8 @@ export class Subject { } } -export default function subject(initialState: T): Subject { +export default function subject(initialState: T, showLog?: boolean): Subject { const subject = new BehaviorSubject(initialState); - return new Subject(initialState, subject); + return new Subject(initialState, subject, showLog); } diff --git a/src/services/stores/who-is-online/index.ts b/src/services/stores/who-is-online/index.ts new file mode 100644 index 00000000..4b93a0f2 --- /dev/null +++ b/src/services/stores/who-is-online/index.ts @@ -0,0 +1,34 @@ +import { Participant } from '../../../common/types/participant.types'; +import { Singleton } from '../common/types'; +import { CreateSingleton } from '../common/utils'; +import subject from '../subject'; + +const instance: Singleton = CreateSingleton(); + +export class WhoIsOnlineStore { + public participantHasJoinedPresence = subject(null); + + constructor() { + if (instance.value) { + throw new Error('CommentsStore is a singleton. There can only be one instance of it.'); + } + + instance.value = this; + } + + public destroy() { + this.participantHasJoinedPresence.destroy(); + instance.value = null; + } +} + +const store = new WhoIsOnlineStore(); +const participantHasJoinedPresence = store.participantHasJoinedPresence.expose(); +const destroy = store.destroy.bind(store); + +export function useWhoIsOnlineStore() { + return { + participantHasJoinedPresence, + destroy, + }; +} diff --git a/src/web-components/base/index.ts b/src/web-components/base/index.ts index 9849bb5f..9b9b450a 100644 --- a/src/web-components/base/index.ts +++ b/src/web-components/base/index.ts @@ -1,5 +1,6 @@ import { LitElement } from 'lit'; +import { useStore } from '../../common/utils/use-store'; import config from '../../services/config'; import { variableStyle, typography, svHr, iconButtonStyle } from './styles'; @@ -7,6 +8,9 @@ import { Constructor, WebComponentsBaseInterface } from './types'; export const WebComponentsBase = >(superClass: T) => { class WebComponentsBaseClass extends superClass { + private unsubscribeFrom: Array<(id: unknown) => void> = []; + protected useStore = useStore.bind(this) as typeof useStore; + static styles = [ variableStyle, typography, @@ -33,6 +37,16 @@ export const WebComponentsBase = >(superClass: super.connectedCallback(); } + /** + * @function disconnectedCallback + * @description Unsubscribes from all the subjects + * @returns {void} + */ + public disconnectedCallback() { + super.disconnectedCallback(); + this.unsubscribeFrom.forEach((unsubscribe) => unsubscribe(this)); + } + /** * @function createCustomColors * @description Creates a custom style tag with the colors from the configuration diff --git a/src/web-components/base/types.ts b/src/web-components/base/types.ts index a1f14df5..8a6dfeca 100644 --- a/src/web-components/base/types.ts +++ b/src/web-components/base/types.ts @@ -1,5 +1,8 @@ +import { Store, StoreType } from '../../common/types/stores.types'; + export type Constructor = new (...args: any[]) => T; export interface WebComponentsBaseInterface { emitEvent(name: string, detail: object, configs?: object): unknown; + useStore(name: T): Store; } diff --git a/src/web-components/comments/components/annotation-pin.test.ts b/src/web-components/comments/components/annotation-pin.test.ts index 50f11879..0b14c5bc 100644 --- a/src/web-components/comments/components/annotation-pin.test.ts +++ b/src/web-components/comments/components/annotation-pin.test.ts @@ -1,5 +1,7 @@ import { MOCK_ANNOTATION } from '../../../../__mocks__/comments.mock'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import sleep from '../../../common/utils/sleep'; +import { useGlobalStore } from '../../../services/stores'; import type { CommentsAnnotationPin } from './annotation-pin'; import { PinMode } from './types'; @@ -40,6 +42,11 @@ function createAnnotationPin({ */ describe('annotation-pin', () => { + beforeEach(() => { + const { localParticipant } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + }); + afterEach(() => { const element = document.getElementsByTagName('superviz-comments-annotation-pin')[0]; diff --git a/src/web-components/comments/components/annotation-pin.ts b/src/web-components/comments/components/annotation-pin.ts index f00563b8..894f809d 100644 --- a/src/web-components/comments/components/annotation-pin.ts +++ b/src/web-components/comments/components/annotation-pin.ts @@ -2,7 +2,8 @@ import { CSSResultGroup, LitElement, PropertyValueMap, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { ParticipantByGroupApi } from '../../../common/types/participant.types'; +import { Participant, ParticipantByGroupApi } from '../../../common/types/participant.types'; +import { StoreType } from '../../../common/types/stores.types'; import { Annotation, PinCoordinates } from '../../../components/comments/types'; import { WebComponentsBase } from '../../base'; import importStyle from '../../base/utils/importStyle'; @@ -141,6 +142,12 @@ export class CommentsAnnotationPin extends WebComponentsBaseElement { if (this.type !== PinMode.ADD) return; + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe((participant: Participant) => { + this.localAvatar = participant?.avatar?.imageUrl; + this.localName = participant?.name; + }); + window.document.body.addEventListener( 'close-temporary-annotation', this.cancelTemporaryAnnotation, diff --git a/src/web-components/who-is-online/components/messages.test.ts b/src/web-components/who-is-online/components/messages.test.ts index b9152f5f..26e80e16 100644 --- a/src/web-components/who-is-online/components/messages.test.ts +++ b/src/web-components/who-is-online/components/messages.test.ts @@ -1,5 +1,7 @@ import '.'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import sleep from '../../../common/utils/sleep'; +import { useGlobalStore } from '../../../services/stores'; import { HorizontalSide, VerticalSide } from './types'; @@ -26,6 +28,9 @@ describe('messages', () => { beforeEach(() => { document.body.innerHTML = ''; + const { localParticipant } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + element = createEl(); }); diff --git a/src/web-components/who-is-online/components/messages.ts b/src/web-components/who-is-online/components/messages.ts index d2c87c86..7cec1062 100644 --- a/src/web-components/who-is-online/components/messages.ts +++ b/src/web-components/who-is-online/components/messages.ts @@ -2,6 +2,8 @@ import { CSSResultGroup, LitElement, PropertyDeclaration, PropertyValueMap, html import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; +import { Participant } from '../../../common/types/participant.types'; +import { StoreType } from '../../../common/types/stores.types'; import { WebComponentsBase } from '../../base'; import importStyle from '../../base/utils/importStyle'; import { messagesStyle } from '../css'; @@ -18,21 +20,28 @@ export class WhoIsOnlineMessages extends WebComponentsBaseElement { declare following: Following | undefined; declare everyoneFollowsMe: boolean; declare isPrivate: boolean; - declare participantColor: string; declare verticalSide: VerticalSide; declare horizontalSide: HorizontalSide; + private participantColor: string; private animationFrame: number | undefined; static properties = { following: { type: Object }, everyoneFollowsMe: { type: Boolean }, isPrivate: { type: Boolean }, - participantColor: { type: String }, verticalSide: { type: String }, horizontalSide: { type: String }, }; + constructor() { + super(); + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe((participant: Participant) => { + this.participantColor = participant.color; + }); + } + protected firstUpdated( _changedProperties: PropertyValueMap | Map, ): void { diff --git a/src/web-components/who-is-online/components/types.ts b/src/web-components/who-is-online/components/types.ts index c4f90b29..f209e7a1 100644 --- a/src/web-components/who-is-online/components/types.ts +++ b/src/web-components/who-is-online/components/types.ts @@ -26,7 +26,6 @@ export interface Options { export interface LocalParticipantData { id: string; - color: string; joinedPresence: boolean; } 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 ff5d59e1..fbb0504b 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 @@ -1,11 +1,15 @@ import { html } from 'lit'; import '.'; -import { MOCK_ABLY_PARTICIPANT_DATA_1 } from '../../../__mocks__/participants.mock'; +import { + MOCK_ABLY_PARTICIPANT_DATA_1, + MOCK_LOCAL_PARTICIPANT, +} from '../../../__mocks__/participants.mock'; import { RealtimeEvent } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; import sleep from '../../common/utils/sleep'; import { Participant } from '../../components/who-is-online/types'; +import { useGlobalStore } from '../../services/stores'; import { Dropdown } from '../dropdown/index'; import { WIODropdownOptions } from './components/types'; @@ -49,6 +53,9 @@ const MOCK_PARTICIPANTS: Participant[] = [ describe('Who Is Online', () => { beforeEach(async () => { + const { localParticipant } = useGlobalStore(); + localParticipant.value = MOCK_LOCAL_PARTICIPANT; + element = document.createElement('superviz-who-is-online'); element['localParticipantData'] = { color: '#fff', @@ -141,7 +148,7 @@ describe('Who Is Online', () => { await sleep(); const letter = element?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[0].slotIndex]; + const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[0].slotIndex as number]; expect(letter?.getAttribute('style')).toBe( `background-color: ${backgroundColor}; color: #26242A`, diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index 2d5b20a6..4e2e6590 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -5,6 +5,7 @@ import { repeat } from 'lit/directives/repeat.js'; import { RealtimeEvent } from '../../common/types/events.types'; import { INDEX_IS_WHITE_TEXT } from '../../common/types/meeting-colors.types'; +import { StoreType } from '../../common/types/stores.types'; import { Participant } from '../../components/who-is-online/types'; import { WebComponentsBase } from '../base'; import importStyle from '../base/utils/importStyle'; @@ -27,7 +28,6 @@ export class WhoIsOnline extends WebComponentsBaseElement { declare localParticipantData: LocalParticipantData; declare isPrivate: boolean; declare everyoneFollowsMe: boolean; - declare showTooltip: boolean; static properties = { @@ -47,6 +47,14 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.position = 'top: 20px; right: 40px;'; this.showTooltip = true; this.open = false; + + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe((value: Participant) => { + this.localParticipantData = { + id: value.id, + joinedPresence: false, + }; + }); } protected firstUpdated( @@ -368,7 +376,6 @@ export class WhoIsOnline extends WebComponentsBaseElement { following=${JSON.stringify(this.following)} ?everyoneFollowsMe=${this.everyoneFollowsMe} ?isPrivate=${this.isPrivate} - participantColor=${this.localParticipantData?.color} @stop-following=${this.stopFollowing} @cancel-private=${this.cancelPrivate} @stop-everyone-follows-me=${this.stopEveryoneFollowsMe} From df79b407e40158bad29cdb63764676b5abec814b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Wed, 6 Mar 2024 20:44:50 -0300 Subject: [PATCH 06/83] fix: call requestUpdating when updating property used in web components --- src/common/utils/use-store.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/utils/use-store.ts b/src/common/utils/use-store.ts index 006d96fa..45cc381b 100644 --- a/src/common/utils/use-store.ts +++ b/src/common/utils/use-store.ts @@ -27,6 +27,8 @@ function subscribeTo( }); this.unsubscribeFrom.push(subject.unsubscribe); + + if (this.requestUpdate) this.requestUpdate(); } /** From 980a43399199ad85f3360d3a8ff8438cf6017fc4 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 7 Mar 2024 07:59:01 -0300 Subject: [PATCH 07/83] fix: remove setTimeout --- src/core/launcher/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 9ad55e8d..fbfb2af4 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -40,10 +40,6 @@ export class Launcher extends Observable implements DefaultLauncher { this.participants = participants; this.group = group; - setTimeout(() => { - localParticipant.value = { ...localParticipant.value, color: 'red' }; - }, 20000); - this.participant.value = { ...participant, type: ParticipantType.GUEST, From 1b13a674b76ce756c47080ee18853fee547eb2cd Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 7 Mar 2024 11:19:15 -0300 Subject: [PATCH 08/83] chore: remove entry points from build config --- .esbuild/config.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.esbuild/config.js b/.esbuild/config.js index e1f2dfb2..dc6b16ea 100644 --- a/.esbuild/config.js +++ b/.esbuild/config.js @@ -5,12 +5,7 @@ const entries = Object.entries(process.env).filter((key) => key[0].startsWith('S const env = Object.fromEntries(entries); module.exports = { - entryPoints: [ - './src/index.ts', - './src/core/index.ts', - './src/components/index.ts', - './src/web-components/index.ts', - ], + entryPoints: ['./src/index.ts'], loader: { '.png': 'file', '.svg': 'file', From ffb19cd17ce2f0edfd25da3f97a0c4284430e3f1 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 7 Mar 2024 11:19:38 -0300 Subject: [PATCH 09/83] feat: attach useStore so 3D adapters use it too --- src/components/base/types.ts | 3 ++- src/core/launcher/index.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/base/types.ts b/src/components/base/types.ts index 5ffb295f..24496c4e 100644 --- a/src/components/base/types.ts +++ b/src/components/base/types.ts @@ -1,4 +1,4 @@ -import { Group, Participant } from '../../common/types/participant.types'; +import { Store, StoreType } from '../../common/types/stores.types'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; import { AblyRealtimeService } from '../../services/realtime'; @@ -8,6 +8,7 @@ export interface DefaultAttachComponentOptions { realtime: AblyRealtimeService; config: Configuration; eventBus: EventBus; + useStore: (name: T) => Store; } export type GlobalStore = { diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index fbfb2af4..a34b995d 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -4,6 +4,7 @@ import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types import { Group, Participant, ParticipantType } from '../../common/types/participant.types'; import { Observable } from '../../common/utils'; import { Logger } from '../../common/utils/logger'; +import { useStore } from '../../common/utils/use-store'; import { BaseComponent } from '../../components/base'; import { ComponentNames } from '../../components/types'; import ApiService from '../../services/api'; @@ -79,6 +80,7 @@ export class Launcher extends Observable implements DefaultLauncher { realtime: this.realtime, config: config.configuration, eventBus: this.eventBus, + useStore, }); this.activeComponents.push(component.name); From 3f9b99d65db2d6d8b57dbb242f2c56c70f3a932b Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 7 Mar 2024 11:20:56 -0300 Subject: [PATCH 10/83] fix: unsubscribe from subjects when destroying room --- src/common/utils/use-store.ts | 2 +- src/components/base/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/utils/use-store.ts b/src/common/utils/use-store.ts index 45cc381b..40d78ed6 100644 --- a/src/common/utils/use-store.ts +++ b/src/common/utils/use-store.ts @@ -1,6 +1,6 @@ import { PublicSubject } from '../../services/stores/common/types'; import { useGlobalStore } from '../../services/stores/global'; -import { Store, StoreType, StoresTypes } from '../types/stores.types'; +import { Store, StoreType } from '../types/stores.types'; const stores = { [StoreType.GLOBAL]: useGlobalStore, diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 30a17bdb..cc38a38b 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -79,6 +79,7 @@ export abstract class BaseComponent extends Observable { this.logger.log('detached'); this.publish(ComponentLifeCycleEvent.UNMOUNT); this.destroy(); + this.unsubscribeFrom.forEach((unsubscribe) => unsubscribe(this)); Object.values(this.observers).forEach((observer) => { observer.reset(); From 27ecc46558446d5d01503c0159f14c9f09cebdf0 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 7 Mar 2024 14:28:57 -0300 Subject: [PATCH 11/83] chore(test-config): only run tests with ts jest --- jest.config.js | 5 +++++ tsconfig.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index a67d5ac9..13f1e82b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,5 +20,10 @@ module.exports = { '/e2e/', '/src/web-components', ], + transformIgnorePatterns: ['node_modules/(?!@superviz/socket-client)'], + transform: { + '^.+\\.ts$': 'ts-jest', + '^.+\\.js$': 'ts-jest', + }, setupFiles: ['/jest.setup.js'], }; diff --git a/tsconfig.json b/tsconfig.json index 90a58f96..33b81dee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "esModuleInterop": true, "moduleResolution": "Node", "experimentalDecorators": true, - "skipLibCheck": true + "skipLibCheck": true, + "allowJs": true, }, "include": [ "./src" From 65359776bb0cda86ff0921f6cc19d411263e15e5 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 7 Mar 2024 14:29:42 -0300 Subject: [PATCH 12/83] feat: initialize io service --- __mocks__/io.mock.ts | 27 ++++++++++ package.json | 4 +- src/core/launcher/index.ts | 4 ++ src/services/io/index.test.ts | 37 ++++++++++++++ src/services/io/index.ts | 64 +++++++++++++++++++++++ src/services/io/types.ts | 0 yarn.lock | 96 +++++++++++++++++++++++++++++++++-- 7 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 __mocks__/io.mock.ts create mode 100644 src/services/io/index.test.ts create mode 100644 src/services/io/index.ts create mode 100644 src/services/io/types.ts diff --git a/__mocks__/io.mock.ts b/__mocks__/io.mock.ts new file mode 100644 index 00000000..ce0b63bf --- /dev/null +++ b/__mocks__/io.mock.ts @@ -0,0 +1,27 @@ +import { jest } from '@jest/globals'; + +export const MOCK_IO = { + Realtime: class { + public connection: { + on: (state: string) => void; + off: () => void; + }; + + constructor(apiKey: string, environment: string, participant: any) { + this.connection = { + on: jest.fn(), + off: jest.fn(), + }; + } + + public connect() { + return { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }; + } + + public destroy() {} + }, +}; diff --git a/package.json b/package.json index 4df129f3..a7eb2c37 100644 --- a/package.json +++ b/package.json @@ -71,12 +71,13 @@ "jest-fetch-mock": "^3.0.3", "rimraf": "^5.0.5", "semantic-release": "^22.0.5", - "ts-jest": "^29.1.1", + "ts-jest": "^29.1.2", "tsc": "^2.0.4", "typescript": "^5.2.2", "yargs": "^17.7.2" }, "dependencies": { + "@superviz/socket-client": "1.2.0", "ably": "^1.2.45", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", @@ -86,6 +87,7 @@ "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.4.3", + "rxjs": "^7.8.1", "semantic-release-version-file": "^1.0.2" }, "config": { diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 3c682a97..a778658f 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -9,6 +9,7 @@ import { ComponentNames } from '../../components/types'; import ApiService from '../../services/api'; import config from '../../services/config'; import { EventBus } from '../../services/event-bus'; +import { IOC } from '../../services/io'; import LimitsService from '../../services/limits'; import { AblyRealtimeService } from '../../services/realtime'; import { AblyParticipant } from '../../services/realtime/ably/types'; @@ -25,6 +26,7 @@ export class Launcher extends Observable implements DefaultLauncher { private participant: Participant; private group: Group; + private ioc: IOC; private realtime: AblyRealtimeService; private eventBus: EventBus = new EventBus(); @@ -45,6 +47,8 @@ export class Launcher extends Observable implements DefaultLauncher { config.get('ablyKey'), ); + this.ioc = new IOC(this.participant); + // internal events without realtime this.eventBus = new EventBus(); diff --git a/src/services/io/index.test.ts b/src/services/io/index.test.ts new file mode 100644 index 00000000..914f85d8 --- /dev/null +++ b/src/services/io/index.test.ts @@ -0,0 +1,37 @@ +import { MOCK_IO } from '../../../__mocks__/io.mock'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; + +import { IOC } from '.'; + +jest.mock('@superviz/socket-client', () => MOCK_IO); + +describe('io', () => { + let instance: IOC | null = null; + + beforeEach(() => { + instance = new IOC(MOCK_LOCAL_PARTICIPANT); + }); + + afterEach(() => { + instance?.destroy(); + instance = null; + }); + + test('should create an instance', () => { + expect(instance).toBeInstanceOf(IOC); + }); + + test('should create a room', () => { + const room = instance?.createRoom('test'); + + expect(room).toBeDefined(); + expect(room).toHaveProperty('on'); + expect(room).toHaveProperty('off'); + expect(room).toHaveProperty('emit'); + }); + + test('should publish state changes', () => { + const next = jest.fn(); + instance?.onStateChange(next); + }); +}); diff --git a/src/services/io/index.ts b/src/services/io/index.ts new file mode 100644 index 00000000..b4411c6a --- /dev/null +++ b/src/services/io/index.ts @@ -0,0 +1,64 @@ +import * as Socket from '@superviz/socket-client'; +import { Subject } from 'rxjs'; + +import { Participant } from '../../common/types/participant.types'; +import config from '../config/index'; + +export class IOC { + public state: Socket.ConnectionState; + public client: Socket.Realtime; + + private stateSubject: Subject = new Subject(); + + constructor(participant: Participant) { + this.client = new Socket.Realtime(config.get('apiKey'), config.get('environment'), { + id: participant.id, + name: participant.name, + }); + + this.subscribeToDefaultEvents(); + } + + /** + * @function destroy + * @description Destroys the socket connection + * @returns {void} + */ + public destroy(): void { + this.client.destroy(); + this.client.connection.off(); + this.client.connection.on((state) => {}); + } + + /** + * @function onStateChange + * @description Subscribe to the socket connection state changes + * @param next {Function} + * @returns {void} + */ + public onStateChange(next: (state: Socket.ConnectionState) => void): void { + this.stateSubject.subscribe(next); + } + + /** + * @function subscribeToDefaultEvents + * @description subscribe to the default socket events + * @returns {void} + */ + private subscribeToDefaultEvents(): void { + this.client.connection.on((state) => { + this.state = state; + this.stateSubject.next(state); + }); + } + + /** + * @function createRoom + * @description create and join realtime room + * @param roomName {string} + * @returns {Room} + */ + public createRoom(roomName: string): Socket.Room { + return this.client.connect(roomName); + } +} diff --git a/src/services/io/types.ts b/src/services/io/types.ts new file mode 100644 index 00000000..e69de29b diff --git a/yarn.lock b/yarn.lock index 6e13cc34..9ffe830c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2315,6 +2315,13 @@ unbzip2-stream "1.4.3" yargs "17.7.1" +"@reactivex/rxjs@^6.6.7": + version "6.6.7" + resolved "https://registry.yarnpkg.com/@reactivex/rxjs/-/rxjs-6.6.7.tgz#52ab48f989aba9cda2b995acc904a43e6e1b3b40" + integrity sha512-xZIV2JgHhWoVPm3uVcFbZDRVJfx2hgqmuTX7J4MuKaZ+j5jN29agniCPBwrlCmpA15/zLKcPi7/bogt0ZwOFyA== + dependencies: + tslib "^1.9.0" + "@rollup/plugin-node-resolve@^15.0.1": version "15.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz#9ffcd8e8c457080dba89bb9fcb583a6778dc757e" @@ -2469,6 +2476,24 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + 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.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.2.0.tgz#091adb3aac2dfd9bc8c7d5bd6ec39fdb0929d7e6" + integrity sha512-1FyJC4wl7nSgHGlC/AduaxTosoM2v/CI5vy9Q5JlBpN3DA5rq1tNwmRB50OrXN/DHKQ0UPimcz1+wIL92Gc5vA== + dependencies: + "@reactivex/rxjs" "^6.6.7" + debug "^4.3.4" + 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" + "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz" @@ -4818,7 +4843,7 @@ debounce@^1.2.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5127,6 +5152,22 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" + integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -9983,6 +10024,13 @@ rxjs@^7.0.0, rxjs@^7.5.5: dependencies: tslib "^2.1.0" +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-array-concat@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" @@ -10241,6 +10289,24 @@ 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== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz" @@ -10811,10 +10877,10 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== -ts-jest@^29.1.1: - version "29.1.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" - integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== +ts-jest@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.2.tgz#7613d8c81c43c8cb312c6904027257e814c40e09" + integrity sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" @@ -10859,6 +10925,11 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.0.1, tslib@^2.4.0: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" @@ -11447,6 +11518,11 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" @@ -11457,6 +11533,11 @@ xmlchars@^2.2.0: resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" @@ -11571,3 +11652,8 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +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== From 812138aaf09e1d3e55db34993f9222ca2e62bdba Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 7 Mar 2024 14:30:32 -0300 Subject: [PATCH 13/83] fix: environment config variable --- src/core/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/index.ts b/src/core/index.ts index 66d3b03a..db885bed 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -126,7 +126,7 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise Date: Thu, 7 Mar 2024 14:39:31 -0300 Subject: [PATCH 14/83] ci: fix remote config step --- .github/workflows/checks.yml | 6 +++--- .github/workflows/publish-beta-release.yml | 2 +- .github/workflows/publish-lab-release.yml | 2 +- .github/workflows/publish-prod-release.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index efb5dc3c..752306c4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,11 +6,11 @@ on: - opened - synchronize jobs: - delete-comments: + delete-comments: runs-on: ubuntu-latest steps: - uses: izhangzhihao/delete-comment@master - with: + with: github_token: ${{ secrets.GITHUB_TOKEN }} delete_user_name: SuperViz-Dev issue_number: ${{ github.event.number }} @@ -28,7 +28,7 @@ jobs: touch .version.js && echo "echo \"export const version = 'test'\" > .version.js" | bash - - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash - - name: Run tests run: yarn test:unit:ci --coverage - name: Code Coverage Report diff --git a/.github/workflows/publish-beta-release.yml b/.github/workflows/publish-beta-release.yml index b7c56c68..6cad78c7 100644 --- a/.github/workflows/publish-beta-release.yml +++ b/.github/workflows/publish-beta-release.yml @@ -19,7 +19,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash - - run: git config --global user.name SuperViz - run: git config --global user.email ci@superviz.com - name: Publish npm package diff --git a/.github/workflows/publish-lab-release.yml b/.github/workflows/publish-lab-release.yml index bc1ccfd0..778e8934 100644 --- a/.github/workflows/publish-lab-release.yml +++ b/.github/workflows/publish-lab-release.yml @@ -19,7 +19,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash - - run: git config --global user.name SuperViz - run: git config --global user.email ci@superviz.com - name: Publish npm package diff --git a/.github/workflows/publish-prod-release.yml b/.github/workflows/publish-prod-release.yml index 81f3aec5..b91356a5 100644 --- a/.github/workflows/publish-prod-release.yml +++ b/.github/workflows/publish-prod-release.yml @@ -19,7 +19,7 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash - - run: git config --global user.name SuperViz - run: git config --global user.email ci@superviz.com - name: Publish npm package From 63df37298d176c95a2c15cf947093c42825922bb Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Thu, 7 Mar 2024 15:20:59 -0300 Subject: [PATCH 15/83] fix: broken tests --- src/components/base/index.test.ts | 8 +++++++- src/components/comments/index.test.ts | 6 ++++-- src/components/presence-mouse/canvas/index.test.ts | 4 +++- src/components/presence-mouse/html/index.test.ts | 4 +++- src/components/realtime/index.test.ts | 6 +++++- src/components/video/index.test.ts | 3 +++ src/components/who-is-online/index.test.ts | 4 ++-- src/core/launcher/index.test.ts | 2 ++ 8 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/base/index.test.ts b/src/components/base/index.test.ts index 9c49f03b..038ce8a4 100644 --- a/src/components/base/index.test.ts +++ b/src/components/base/index.test.ts @@ -3,8 +3,8 @@ import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; -import { Group } from '../../common/types/participant.types'; import { Logger } from '../../common/utils'; +import { useStore } from '../../common/utils/use-store'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; import { AblyRealtimeService } from '../../services/realtime'; @@ -66,6 +66,7 @@ describe('BaseComponent', () => { realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); DummyComponentInstance['start'] = jest.fn(DummyComponentInstance['start']); @@ -88,6 +89,7 @@ describe('BaseComponent', () => { realtime: ablyMock as AblyRealtimeService, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); DummyComponentInstance['start'] = jest.fn(); @@ -105,6 +107,7 @@ describe('BaseComponent', () => { realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); expect(DummyComponentInstance['realtime']).toEqual(REALTIME_MOCK); @@ -120,6 +123,7 @@ describe('BaseComponent', () => { realtime: null as unknown as AblyRealtimeService, config: null as unknown as Configuration, eventBus: null as unknown as EventBus, + useStore: null as unknown as typeof useStore, }); }).toThrowError(); }); @@ -134,6 +138,7 @@ describe('BaseComponent', () => { realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); DummyComponentInstance.detach(); @@ -153,6 +158,7 @@ describe('BaseComponent', () => { realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); DummyComponentInstance.subscribe('test', callback); diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index 29388eda..1377d66f 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -1,5 +1,3 @@ -import { TestScheduler } from 'rxjs/testing'; - import { MOCK_ANNOTATION } from '../../../__mocks__/comments.mock'; import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; @@ -9,6 +7,7 @@ import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { CommentEvent } from '../../common/types/events.types'; import { ParticipantByGroupApi } from '../../common/types/participant.types'; import sleep from '../../common/utils/sleep'; +import { useStore } from '../../common/utils/use-store'; import ApiService from '../../services/api'; import { useGlobalStore } from '../../services/stores'; import { CommentsFloatButton } from '../../web-components'; @@ -81,6 +80,7 @@ describe('Comments', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); commentsComponent['element'].updateAnnotations = jest.fn(); @@ -333,6 +333,7 @@ describe('Comments', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); await sleep(1); @@ -350,6 +351,7 @@ describe('Comments', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); await sleep(1); diff --git a/src/components/presence-mouse/canvas/index.test.ts b/src/components/presence-mouse/canvas/index.test.ts index 1145f6b7..2bf27146 100644 --- a/src/components/presence-mouse/canvas/index.test.ts +++ b/src/components/presence-mouse/canvas/index.test.ts @@ -1,8 +1,9 @@ import { MOCK_CANVAS } from '../../../../__mocks__/canvas.mock'; 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'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../../__mocks__/realtime.mock'; +import { useStore } from '../../../common/utils/use-store'; import { ParticipantMouse } from '../types'; import { PointersCanvas } from './index'; @@ -35,6 +36,7 @@ const createMousePointers = (): PointersCanvas => { realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); presenceMouseComponent['canvas'] = { diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index ff7025d4..cf758892 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -1,8 +1,9 @@ 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'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../../__mocks__/realtime.mock'; import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; +import { useStore } from '../../../common/utils/use-store'; import { ParticipantMouse, Element } from '../types'; import { PointersHTML } from '.'; @@ -15,6 +16,7 @@ const createMousePointers = (): PointersHTML => { realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); return presenceMouseComponent; diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index 916678c8..8ffc4ae7 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -1,7 +1,7 @@ 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'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; +import { useStore } from '../../common/utils/use-store'; import { Realtime } from '.'; @@ -31,6 +31,7 @@ describe('realtime component', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); }); @@ -76,6 +77,7 @@ describe('realtime component', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); RealtimeComponentInstance.subscribe('test', callback); @@ -94,6 +96,7 @@ describe('realtime component', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); RealtimeComponentInstance.subscribe('test', callback); @@ -118,6 +121,7 @@ describe('realtime component', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); RealtimeComponentInstance.publish('test', 'test'); diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index c2900cdd..ce975a2b 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -15,6 +15,7 @@ import { } from '../../common/types/events.types'; import { MeetingColors } from '../../common/types/meeting-colors.types'; import { ParticipantType } from '../../common/types/participant.types'; +import { useStore } from '../../common/utils/use-store'; import { AblyParticipant, AblyRealtimeData } from '../../services/realtime/ably/types'; import { VideoFrameState } from '../../services/video-conference-manager/types'; import { ComponentNames } from '../types'; @@ -97,6 +98,7 @@ describe('VideoConference', () => { realtime: MOCK_REALTIME, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); }); @@ -120,6 +122,7 @@ describe('VideoConference', () => { realtime: MOCK_REALTIME, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); expect(VideoConferenceInstance['videoConfig'].canUseDefaultAvatars).toBeFalsy(); diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index 94834911..966af98e 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -3,14 +3,13 @@ import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_ABLY_PARTICIPANT, MOCK_ABLY_PARTICIPANT_DATA_2, - MOCK_GROUP, MOCK_LOCAL_PARTICIPANT, MOCK_ABLY_PARTICIPANT_DATA_1, } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; -import { useGlobalStore } from '../../services/stores'; +import { useStore } from '../../common/utils/use-store'; import { WhoIsOnline } from './index'; @@ -25,6 +24,7 @@ describe('Who Is Online', () => { realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); whoIsOnlineComponent['localParticipantId'] = MOCK_LOCAL_PARTICIPANT.id; diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 084f75cf..4f8f7788 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -3,6 +3,7 @@ import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types'; +import { useStore } from '../../common/utils/use-store'; import { BaseComponent } from '../../components/base'; import { ComponentNames } from '../../components/types'; import LimitsService from '../../services/limits'; @@ -96,6 +97,7 @@ describe('Launcher', () => { realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, + useStore, }); expect(ABLY_REALTIME_MOCK.updateMyProperties).toHaveBeenCalledWith({ From cbf5e301fdae9e9c5447632de902574396ab46c6 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 7 Mar 2024 16:18:11 -0300 Subject: [PATCH 16/83] feat: perform participant changes into new io --- src/core/launcher/index.ts | 56 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index a778658f..25ad27be 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -1,3 +1,4 @@ +import * as Socket from '@superviz/socket-client'; import { isEqual } from 'lodash'; import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types'; @@ -27,6 +28,7 @@ export class Launcher extends Observable implements DefaultLauncher { private group: Group; private ioc: IOC; + private LaucherRealtimeRoom: Socket.Room; private realtime: AblyRealtimeService; private eventBus: EventBus = new EventBus(); @@ -42,19 +44,24 @@ export class Launcher extends Observable implements DefaultLauncher { this.group = group; this.logger = new Logger('@superviz/sdk/launcher'); + + // Ably realtime service this.realtime = new AblyRealtimeService( config.get('apiUrl'), config.get('ablyKey'), ); + // SuperViz IO Room this.ioc = new IOC(this.participant); + this.LaucherRealtimeRoom = this.ioc.createRoom('launcher'); // internal events without realtime this.eventBus = new EventBus(); this.logger.log('launcher created'); - this.startRealtime(); + this.startAbly(); + this.startIOC(); } /** @@ -126,6 +133,7 @@ export class Launcher extends Observable implements DefaultLauncher { return c.name !== component.name; }); this.activeComponents.splice(this.activeComponents.indexOf(component.name), 1); + this.realtime.updateMyProperties({ activeComponents: this.activeComponents }); }; @@ -202,12 +210,12 @@ export class Launcher extends Observable implements DefaultLauncher { }; /** - * @function startRealtime + * @function startAbly * @description start realtime service and join to room * @returns {void} */ - private startRealtime = (): void => { - this.logger.log('launcher service @ startRealtime'); + private startAbly = (): void => { + this.logger.log('launcher service @ startAbly'); this.realtime.start({ participant: this.participant, @@ -218,15 +226,15 @@ export class Launcher extends Observable implements DefaultLauncher { this.realtime.join(); // subscribe to realtime events - this.subscribeToRealtimeEvents(); + this.subscribeToAblyEvents(); }; /** - * @function subscribeToRealtimeEvents + * @function subscribeToAblyEvents * @description subscribe to realtime events * @returns {void} */ - private subscribeToRealtimeEvents = (): void => { + private subscribeToAblyEvents = (): void => { this.realtime.authenticationObserver.subscribe(this.onAuthentication); this.realtime.sameAccountObserver.subscribe(this.onSameAccount); this.realtime.participantJoinedObserver.subscribe(this.onParticipantJoined); @@ -234,7 +242,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.realtime.participantsObserver.subscribe(this.onParticipantListUpdate); }; - /** Realtime Listeners */ + /** Ably Listeners */ private onAuthentication = (event: RealtimeEvent): void => { if (event !== RealtimeEvent.REALTIME_AUTHENTICATION_FAILED) return; @@ -288,6 +296,8 @@ export class Launcher extends Observable implements DefaultLauncher { return this.activeComponents.includes(component.name); }); + + this.LaucherRealtimeRoom.presence.update(localParticipant); this.participant = localParticipant; this.publish(ParticipantEvent.LOCAL_UPDATED, localParticipant); @@ -353,6 +363,36 @@ export class Launcher extends Observable implements DefaultLauncher { this.publish(ParticipantEvent.SAME_ACCOUNT_ERROR); this.destroy(); }; + + /** New IO */ + + /** + * @function startIOC + * @description start IO service + * @returns {void} + */ + + private startIOC = (): void => { + this.logger.log('launcher service @ startIOC'); + + this.LaucherRealtimeRoom.presence.on( + Socket.PresenceEvents.JOINED_ROOM, + this.onParticipantJoinedIOC, + ); + + this.LaucherRealtimeRoom.presence.on(Socket.PresenceEvents.UPDATE, () => {}); + this.LaucherRealtimeRoom.presence.on(Socket.PresenceEvents.LEAVE, () => {}); + }; + + private onParticipantJoinedIOC = (presence: Socket.PresenceEvent) => { + if (presence.id === this.participant.id) { + this.onLocalParticipantJoined(presence); + } + }; + + private onLocalParticipantJoined = (_: Socket.PresenceEvent) => { + this.LaucherRealtimeRoom.presence.update(this.participant); + }; } /** From 237545e9a1e141ff79612e8a5d7bb6a48ab10f5e Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 7 Mar 2024 16:20:58 -0300 Subject: [PATCH 17/83] fix: concats the room name with global room id --- src/services/io/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/io/index.ts b/src/services/io/index.ts index b4411c6a..f85cddea 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -59,6 +59,7 @@ export class IOC { * @returns {Room} */ public createRoom(roomName: string): Socket.Room { - return this.client.connect(roomName); + const roomId = config.get('roomId'); + return this.client.connect(`${roomId}:${roomName}`); } } From d64ace159a1fc31b6069413c8ed85ea9fe08117f Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 11:30:20 -0300 Subject: [PATCH 18/83] feat: new slots management --- src/common/types/participant.types.ts | 9 ++ src/core/launcher/index.ts | 15 +-- src/services/slot/index.test.ts | 140 ++++++++++++++++++++++++++ src/services/slot/index.ts | 98 ++++++++++++++++++ src/services/slot/type.ts | 0 5 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 src/services/slot/index.test.ts create mode 100644 src/services/slot/index.ts create mode 100644 src/services/slot/type.ts diff --git a/src/common/types/participant.types.ts b/src/common/types/participant.types.ts index ac6825ad..0622e82c 100644 --- a/src/common/types/participant.types.ts +++ b/src/common/types/participant.types.ts @@ -6,11 +6,20 @@ export enum ParticipantType { AUDIENCE = 'audience', } +export type Slot = { + index: number; + color: string; + textColor: string; + colorName: string; + timestamp: number; +}; + export interface Participant { id: string; name?: string; type?: ParticipantType; color?: string; + slot?: Slot; avatar?: Avatar; isHost?: boolean; // @NOTE - this is a hack to make the participant info work with the 3D avatar diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 25ad27be..b61f3819 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -14,6 +14,8 @@ import { IOC } from '../../services/io'; import LimitsService from '../../services/limits'; import { AblyRealtimeService } from '../../services/realtime'; import { AblyParticipant } from '../../services/realtime/ably/types'; +import { ParticipantInfo } from '../../services/realtime/base/types'; +import { SlotService } from '../../services/slot'; import { DefaultLauncher, LauncherFacade, LauncherOptions } from './types'; @@ -379,18 +381,17 @@ export class Launcher extends Observable implements DefaultLauncher { Socket.PresenceEvents.JOINED_ROOM, this.onParticipantJoinedIOC, ); - - this.LaucherRealtimeRoom.presence.on(Socket.PresenceEvents.UPDATE, () => {}); - this.LaucherRealtimeRoom.presence.on(Socket.PresenceEvents.LEAVE, () => {}); }; private onParticipantJoinedIOC = (presence: Socket.PresenceEvent) => { - if (presence.id === this.participant.id) { - this.onLocalParticipantJoined(presence); - } + if (presence.id !== this.participant.id) return; + + this.onLocalParticipantJoined(); }; - private onLocalParticipantJoined = (_: Socket.PresenceEvent) => { + private onLocalParticipantJoined = () => { + // Assign a slot to the participant + const _ = new SlotService(this.LaucherRealtimeRoom, this.participant); this.LaucherRealtimeRoom.presence.update(this.participant); }; } diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts new file mode 100644 index 00000000..faa16f1d --- /dev/null +++ b/src/services/slot/index.test.ts @@ -0,0 +1,140 @@ +import { SlotService } from '.'; + +describe('slot service', () => { + it('should assign a slot to the participant', async () => { + const room = { + presence: { + on: jest.fn(), + get: jest.fn((callback) => { + callback({ + presences: [], + }); + }), + update: jest.fn(), + }, + } as any; + + const participant = { + id: '123', + } as any; + + const instance = new SlotService(room, participant); + await instance['assignSlot'](); + + expect(instance['slotIndex']).toBeDefined(); + expect(instance['participant'].slot).toBeDefined(); + expect(room.presence.update).toHaveBeenCalledWith({ + slot: expect.objectContaining({ + index: expect.any(Number), + color: expect.any(String), + textColor: expect.any(String), + colorName: expect.any(String), + timestamp: expect.any(Number), + }), + }); + }); + + it('should assign a slot to the participant', async () => { + const room = { + presence: { + on: jest.fn(), + get: jest.fn((callback) => { + callback({ + presences: [], + }); + }), + update: jest.fn(), + }, + } as any; + + const participant = { + id: '123', + } as any; + + const instance = new SlotService(room, participant); + await instance['assignSlot'](); + + expect(instance['slotIndex']).toBeDefined(); + expect(instance['participant'].slot).toBeDefined(); + expect(room.presence.update).toHaveBeenCalledWith({ + slot: expect.objectContaining({ + index: expect.any(Number), + color: expect.any(String), + textColor: expect.any(String), + colorName: expect.any(String), + timestamp: expect.any(Number), + }), + }); + }); + + it('if there are no more slots available, it should throw an error', async () => { + console.error = jest.fn(); + + const room = { + presence: { + on: jest.fn(), + get: jest.fn((callback) => { + callback({ + presences: new Array(16).fill({}), + }); + }), + update: jest.fn(), + }, + } as any; + + const participant = { + id: '123', + } as any; + + const instance = new SlotService(room, participant); + await instance['assignSlot'](); + + expect(instance['slotIndex']).toBeUndefined(); + expect(instance['participant'].slot).toBeUndefined(); + }); + + it('if the slot is already in use, it should assign a new slot', async () => { + const room = { + presence: { + on: jest.fn(), + get: jest.fn((callback) => { + callback({ + presences: [ + { + data: { + slot: { + index: 0, + }, + }, + }, + ], + }); + }), + update: jest.fn(), + }, + } as any; + + const mockMath = Object.create(global.Math); + mockMath.random = jest.fn().mockReturnValueOnce(0).mockReturnValueOnce(1); + global.Math = mockMath; + + const participant = { + id: '123', + } as any; + + const instance = new SlotService(room, participant); + await instance['assignSlot'](); + + expect(instance['slotIndex']).toBeDefined(); + expect(instance['participant'].slot).toBeDefined(); + expect(room.presence.update).toHaveBeenCalledWith({ + slot: expect.objectContaining({ + index: expect.any(Number), + 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 new file mode 100644 index 00000000..ad0da1f5 --- /dev/null +++ b/src/services/slot/index.ts @@ -0,0 +1,98 @@ +import * as Socket from '@superviz/socket-client'; + +import { + INDEX_IS_WHITE_TEXT, + MeetingColors, + MeetingColorsHex, +} from '../../common/types/meeting-colors.types'; +import { Participant } from '../../common/types/participant.types'; + +export class SlotService { + private room: Socket.Room; + private participant: Participant; + private slotIndex: number; + + constructor(room: Socket.Room, participant: Participant) { + this.room = room; + this.participant = participant; + + this.assignSlot(); + this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); + } + + /** + * @function assignSlot + * @description Assigns a slot to the participant + * @returns void + */ + private async assignSlot() { + const slot = Math.floor(Math.random() * 16); + + new Promise((resolve, reject) => { + this.room.presence.get((event: any) => { + if (!event.presences || !event.presences.length) resolve(false); + + if (event.presences.length >= 16) { + reject(new Error('[SuperViz] - No more slots available')); + return; + } + + event.presences.forEach((presence: Socket.PresenceEvent) => { + if (presence.id === this.participant.id) return; + + if (presence.data?.slot?.index === slot) resolve(true); + }); + + resolve(false); + }); + }) + .then((isUsing) => { + if (isUsing) { + this.assignSlot(); + return; + } + + const slotData = { + index: slot, + color: MeetingColorsHex[slot], + textColor: INDEX_IS_WHITE_TEXT.includes(slot) ? '#fff' : '#000', + colorName: MeetingColors[slot], + timestamp: Date.now(), + }; + + this.slotIndex = slot; + this.participant = { + ...this.participant, + slot: slotData, + }; + + this.room.presence.update({ + slot: slotData, + }); + }) + .catch((error) => { + this.room.presence.update({ + slot: null, + }); + console.error(error); + }); + } + + private onPresenceUpdate = (event: Socket.PresenceEvent) => { + if (!event.data.slot || !this.participant?.slot) return; + + if (event.id === this.participant.id) { + this.participant = event.data; + this.slotIndex = event.data.slot.index; + return; + } + + const assignFisrt = event.data.slot.timestamp < this.participant?.slot?.timestamp; + + // if someone else has the same slot as me, and they were assigned first, I should reassign + if (event.data.slot?.index === this.slotIndex && assignFisrt) { + console.log('Reassigning slot'); + this.assignSlot(); + } + }; +} diff --git a/src/services/slot/type.ts b/src/services/slot/type.ts new file mode 100644 index 00000000..e69de29b From 82a2d0a5fd511210b8e37dafa33625b62e216292 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 11:31:00 -0300 Subject: [PATCH 19/83] ci: fix build step --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 752306c4..7b1c5b19 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -70,6 +70,6 @@ jobs: touch .version.js && echo "echo \"export const version = 'test'\" > .version.js" | bash - - name: Create a .remote-config.js file run: | - touch .remote-config.js && echo "echo \"export default { apiUrl: 'https://localhost:3000', conferenceLayerUrl: 'https://localhost:8080' }\" > .remote-config.js" | bash - + touch .remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash - - name: Build run: yarn build From 04b0f4690d22834c2f799a2da7a04701c695b920 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 13:07:14 -0300 Subject: [PATCH 20/83] fix: remove log and spell check --- src/services/slot/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index ad0da1f5..2891257b 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -87,11 +87,10 @@ export class SlotService { return; } - const assignFisrt = event.data.slot.timestamp < this.participant?.slot?.timestamp; + const assignFirst = event.data.slot.timestamp < this.participant?.slot?.timestamp; // if someone else has the same slot as me, and they were assigned first, I should reassign - if (event.data.slot?.index === this.slotIndex && assignFisrt) { - console.log('Reassigning slot'); + if (event.data.slot?.index === this.slotIndex && assignFirst) { this.assignSlot(); } }; From cb024208c726f7f52ca41fe6b109ab50b8c0a0c7 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 13:44:43 -0300 Subject: [PATCH 21/83] feat: update ioc version --- package.json | 2 +- src/services/slot/index.test.ts | 28 ++++++++++------------------ src/services/slot/index.ts | 8 ++++---- yarn.lock | 8 ++++---- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index a7eb2c37..c78559c4 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.2.0", + "@superviz/socket-client": "1.2.1", "ably": "^1.2.45", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index faa16f1d..1c8ef002 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -6,9 +6,7 @@ describe('slot service', () => { presence: { on: jest.fn(), get: jest.fn((callback) => { - callback({ - presences: [], - }); + callback([]); }), update: jest.fn(), }, @@ -39,9 +37,7 @@ describe('slot service', () => { presence: { on: jest.fn(), get: jest.fn((callback) => { - callback({ - presences: [], - }); + callback([]); }), update: jest.fn(), }, @@ -74,9 +70,7 @@ describe('slot service', () => { presence: { on: jest.fn(), get: jest.fn((callback) => { - callback({ - presences: new Array(16).fill({}), - }); + callback(new Array(16).fill({})); }), update: jest.fn(), }, @@ -98,17 +92,15 @@ describe('slot service', () => { presence: { on: jest.fn(), get: jest.fn((callback) => { - callback({ - presences: [ - { - data: { - slot: { - index: 0, - }, + callback([ + { + data: { + slot: { + index: 0, }, }, - ], - }); + }, + ]); }), update: jest.fn(), }, diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 2891257b..3305c237 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -29,15 +29,15 @@ export class SlotService { const slot = Math.floor(Math.random() * 16); new Promise((resolve, reject) => { - this.room.presence.get((event: any) => { - if (!event.presences || !event.presences.length) resolve(false); + this.room.presence.get((presences) => { + if (!presences || !presences.length) resolve(false); - if (event.presences.length >= 16) { + if (presences.length >= 16) { reject(new Error('[SuperViz] - No more slots available')); return; } - event.presences.forEach((presence: Socket.PresenceEvent) => { + presences.forEach((presence: Socket.PresenceEvent) => { if (presence.id === this.participant.id) return; if (presence.data?.slot?.index === slot) resolve(true); diff --git a/yarn.lock b/yarn.lock index 9ffe830c..373e0dd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2481,10 +2481,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.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.2.0.tgz#091adb3aac2dfd9bc8c7d5bd6ec39fdb0929d7e6" - integrity sha512-1FyJC4wl7nSgHGlC/AduaxTosoM2v/CI5vy9Q5JlBpN3DA5rq1tNwmRB50OrXN/DHKQ0UPimcz1+wIL92Gc5vA== +"@superviz/socket-client@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.2.1.tgz#9a4ceb9bc2c07361210756e66eb866991e8ceaed" + integrity sha512-8H9APdKPxyPncJmwTshu+VxzBxHCiLP/5TwAkWLnr/f5cPPsRxrm/7Ws51y1yiObz9bNkniwphER6qV4dUdCOw== dependencies: "@reactivex/rxjs" "^6.6.7" debug "^4.3.4" From 71bf57d20a83b5305537e4b99bf039720f203811 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 19:14:53 -0300 Subject: [PATCH 22/83] refactor: unsused vars and types --- src/services/slot/index.ts | 4 ++-- src/services/stores/common/types.ts | 12 ++++++------ src/services/stores/global/index.ts | 2 +- src/services/stores/subject/index.ts | 13 +++---------- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 3305c237..3a36d549 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -87,10 +87,10 @@ export class SlotService { return; } - const assignFirst = event.data.slot.timestamp < this.participant?.slot?.timestamp; + const slotOccupied = event.data.slot.timestamp < this.participant?.slot?.timestamp; // if someone else has the same slot as me, and they were assigned first, I should reassign - if (event.data.slot?.index === this.slotIndex && assignFirst) { + if (event.data.slot?.index === this.slotIndex && slotOccupied) { this.assignSlot(); } }; diff --git a/src/services/stores/common/types.ts b/src/services/stores/common/types.ts index 9af80afc..11cace99 100644 --- a/src/services/stores/common/types.ts +++ b/src/services/stores/common/types.ts @@ -1,10 +1,10 @@ -type callback = (a: T, b?: K) => void; +type Callback = (a: T, b?: K) => void; export type SimpleSubject = { value: T; - publish: callback; - subscribe: callback>; - unsubscribe: callback; + publish: Callback; + subscribe: Callback>; + unsubscribe: Callback; }; export type Singleton = { @@ -15,6 +15,6 @@ export type Singleton = { export type PublicSubject = { get value(): T; set value(T); - subscribe: callback>; - unsubscribe: callback; + subscribe: Callback>; + unsubscribe: Callback; }; diff --git a/src/services/stores/global/index.ts b/src/services/stores/global/index.ts index e8298089..66009181 100644 --- a/src/services/stores/global/index.ts +++ b/src/services/stores/global/index.ts @@ -1,5 +1,5 @@ import { Group, Participant } from '../../../common/types/participant.types'; -import { PublicSubject, Singleton } from '../common/types'; +import { Singleton } from '../common/types'; import { CreateSingleton } from '../common/utils'; import subject from '../subject'; diff --git a/src/services/stores/subject/index.ts b/src/services/stores/subject/index.ts index a73d47ab..957f6fb8 100644 --- a/src/services/stores/subject/index.ts +++ b/src/services/stores/subject/index.ts @@ -9,32 +9,25 @@ export class Subject { private subscriptions: Map = new Map(); private showLog: boolean; - constructor(state: T, _subject: BehaviorSubject, showLog?: boolean) { + constructor(state: T, subject: BehaviorSubject, showLog?: boolean) { this.state = state; this.showLog = !!showLog; - this.subject = _subject.pipe( + this.subject = subject.pipe( distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }), - ) as any; + ) as BehaviorSubject; } private getValue(): T { return this.state; } - private ids: any[] = []; - - private num = Math.floor(Math.random() * 100); private setValue = (newValue: T): void => { this.state = newValue; this.subject.next(this.state); }; - private counter = 0; - public subscribe = (subscriptionId: string | this, callback: (value: T) => void) => { - this.ids.push(subscriptionId); - const number = Math.floor(Math.random() * 100); const subscription = this.subject.subscribe(callback); this.subscriptions.set(subscriptionId, subscription); }; From 39dea93219796603abf8be82f346e066ab4b4cf7 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 8 Mar 2024 19:47:58 -0300 Subject: [PATCH 23/83] fix: export realtime component from root --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 0dbe81f9..8c17e31f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -102,6 +102,7 @@ export { MousePointers, WhoIsOnline, VideoConference, + Realtime, }; export default init; From 3821fec28ffe791ca297c9fd8c726dbc843793af Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 11 Mar 2024 11:37:25 -0300 Subject: [PATCH 24/83] feat: update participant list by new IOC --- src/core/launcher/index.test.ts | 132 ++++++++-------------------- src/core/launcher/index.ts | 111 ++++++++++++++--------- src/services/stores/global/index.ts | 2 +- 3 files changed, 108 insertions(+), 137 deletions(-) diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 4f8f7788..30112163 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -1,8 +1,13 @@ 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'; +import { + MOCK_ABLY_PARTICIPANT, + MOCK_GROUP, + MOCK_LOCAL_PARTICIPANT, +} from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types'; +import { Participant } from '../../common/types/participant.types'; import { useStore } from '../../common/utils/use-store'; import { BaseComponent } from '../../components/base'; import { ComponentNames } from '../../components/types'; @@ -151,65 +156,30 @@ describe('Launcher', () => { describe('Participant Events', () => { test('should publish ParticipantEvent.JOINED event', () => { - const callback = jest.fn(); - const spy = jest.spyOn(LauncherInstance, 'subscribe'); - LauncherInstance['publish'] = jest.fn(); - - LauncherInstance.subscribe(ParticipantEvent.LIST_UPDATED, callback); - - LauncherInstance['onParticipantListUpdate']({ - participant1: { - extras: null, - clientId: 'client1', - action: 'present', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - participantId: 'participant1', - }, - }, - }); - - expect(spy).toHaveBeenCalledWith(ParticipantEvent.LIST_UPDATED, callback); - expect(LauncherInstance['publish']).toHaveBeenCalled(); - }); - - test('should publish ParticipantEvent.LOCAL_UPDATED event', () => { + LauncherInstance['participants'].value = new Map(); + LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); const spy = jest.spyOn(LauncherInstance, 'subscribe'); LauncherInstance['publish'] = jest.fn(); const callback = jest.fn(); LauncherInstance.subscribe(ParticipantEvent.JOINED, callback); - LauncherInstance['onParticipantListUpdate']({ - [MOCK_LOCAL_PARTICIPANT.id]: { - extras: null, - clientId: 'client1', - action: 'present', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - id: MOCK_LOCAL_PARTICIPANT.id, - }, - }, - }); + LauncherInstance['onParticipantJoined'](MOCK_ABLY_PARTICIPANT); expect(spy).toHaveBeenCalledWith(ParticipantEvent.JOINED, callback); expect(LauncherInstance['publish']).toHaveBeenCalledTimes(2); }); test('should publish ParticipantEvent.LEFT event', () => { + LauncherInstance['participants'].value = new Map(); + LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); const callback = jest.fn(); const spy = jest.spyOn(LauncherInstance, 'subscribe'); LauncherInstance['publish'] = jest.fn(); LauncherInstance.subscribe(ParticipantEvent.LEFT, callback); const participant = { - clientId: 'client1', + clientId: MOCK_ABLY_PARTICIPANT.clientId, action: 'absent', connectionId: 'connection1', encoding: 'h264', @@ -220,9 +190,6 @@ describe('Launcher', () => { }, }; - LauncherInstance['onParticipantListUpdate']({ - [participant.data.participantId]: participant as AblyParticipant, - }); LauncherInstance['onParticipantLeave'](participant as AblyParticipant); expect(spy).toHaveBeenCalledWith(ParticipantEvent.LEFT, callback); @@ -255,13 +222,15 @@ describe('Launcher', () => { }); test('should publish ParticipantEvent.LOCAL_LEFT event', () => { + LauncherInstance['participants'].value = new Map(); + LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); const callback = jest.fn(); const spy = jest.spyOn(LauncherInstance, 'subscribe'); LauncherInstance['publish'] = jest.fn(); - LauncherInstance.subscribe(ParticipantEvent.LEFT, callback); + LauncherInstance.subscribe(ParticipantEvent.LOCAL_LEFT, callback); const participant = { - clientId: 'client1', + clientId: MOCK_LOCAL_PARTICIPANT.id, action: 'absent', connectionId: 'connection1', encoding: 'h264', @@ -273,44 +242,16 @@ describe('Launcher', () => { }, }; - LauncherInstance['onParticipantListUpdate']({ - [MOCK_LOCAL_PARTICIPANT.id]: participant as AblyParticipant, - }); LauncherInstance['onParticipantLeave'](participant as AblyParticipant); - expect(spy).toHaveBeenCalledWith(ParticipantEvent.LEFT, callback); - expect(LauncherInstance['publish']).toHaveBeenCalled(); - }); - - test('should publish ParticipantEvent.JOINED event', () => { - const callback = jest.fn(); - const spy = jest.spyOn(LauncherInstance, 'subscribe'); - LauncherInstance['publish'] = jest.fn(); - LauncherInstance.subscribe(ParticipantEvent.JOINED, callback); - - const participant = { - clientId: 'client1', - action: 'absent', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - id: MOCK_LOCAL_PARTICIPANT.id, - participantId: MOCK_LOCAL_PARTICIPANT.id, - }, - }; - - LauncherInstance['onParticipantListUpdate']({ - [MOCK_LOCAL_PARTICIPANT.id]: participant as AblyParticipant, - }); - LauncherInstance['onParticipantJoined'](participant as AblyParticipant); - - expect(spy).toHaveBeenCalledWith(ParticipantEvent.JOINED, callback); + expect(spy).toHaveBeenCalledWith(ParticipantEvent.LOCAL_LEFT, callback); expect(LauncherInstance['publish']).toHaveBeenCalled(); }); test('should skip and publish ParticipantEvent.JOINED event', () => { + LauncherInstance['participants'].value = new Map(); + LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); + const callback = jest.fn(); const spy = jest.spyOn(LauncherInstance, 'subscribe'); LauncherInstance['publish'] = jest.fn(); @@ -360,21 +301,24 @@ describe('Launcher', () => { test('should remove component when participant is not usign it anymore', () => { LauncherInstance.addComponent(MOCK_COMPONENT); - LauncherInstance['onParticipantListUpdate']({ - participant1: { - extras: null, - clientId: MOCK_LOCAL_PARTICIPANT.id, - action: 'present', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - ...MOCK_LOCAL_PARTICIPANT, - participantId: MOCK_LOCAL_PARTICIPANT.id, - activeComponents: [], - }, - }, + LauncherInstance['onParticipantUpdatedIOC']({ + connectionId: 'connection1', + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + data: MOCK_LOCAL_PARTICIPANT as Participant, + timestamp: Date.now(), + }); + + expect(LauncherInstance['activeComponentsInstances'].length).toBe(1); + + LauncherInstance.removeComponent(MOCK_COMPONENT); + + LauncherInstance['onParticipantUpdatedIOC']({ + connectionId: 'connection1', + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + data: MOCK_LOCAL_PARTICIPANT as Participant, + timestamp: Date.now(), }); expect(LauncherInstance['activeComponentsInstances'].length).toBe(0); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 2ac1f2d5..90adbdab 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -36,7 +36,7 @@ export class Launcher extends Observable implements DefaultLauncher { private eventBus: EventBus = new EventBus(); private participant: PublicSubject; - private participants: PublicSubject; + private participants: PublicSubject>; private group: PublicSubject; constructor({ participant, group: participantGroup }: LauncherOptions) { @@ -292,37 +292,9 @@ export class Launcher extends Observable implements DefaultLauncher { return participant?.id === this.participant.value?.id; }); - if (!isEqual(this.participants.value, participantList)) { - this.participants.value = participantList; - this.publish(ParticipantEvent.LIST_UPDATED, participantList); - - this.logger.log('Publishing ParticipantEvent.LIST_UPDATED', participantList); - } - if (localParticipant && !isEqual(this.participant.value, localParticipant)) { - this.activeComponents = localParticipant.activeComponents ?? []; - 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 (!localParticipant.activeComponents) return true; - - return this.activeComponents.includes(component.name); - }); - this.LaucherRealtimeRoom.presence.update(localParticipant); - this.participant.value = localParticipant; - this.publish(ParticipantEvent.LOCAL_UPDATED, localParticipant); - - this.logger.log('Publishing ParticipantEvent.UPDATED', localParticipant); } - - this.logger.log( - 'launcher service @ onParticipantListUpdate - participants updated', - participantList, - ); }; /** @@ -334,9 +306,7 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoined = (ablyParticipant: AblyParticipant): void => { this.logger.log('launcher service @ onParticipantJoined'); - const participant = this.participants.value.find( - (participant) => participant.id === ablyParticipant.data.id, - ); + const participant = this.participants.value.get(ablyParticipant.clientId); if (!participant) return; @@ -358,9 +328,7 @@ export class Launcher extends Observable implements DefaultLauncher { */ private onParticipantLeave = (ablyParticipant: AblyParticipant): void => { this.logger.log('launcher service @ onParticipantLeave'); - const participant = this.participants.value.find( - (participant) => participant.id === ablyParticipant.data.id, - ); + const participant = this.participants.value.get(ablyParticipant.clientId); if (!participant) return; @@ -393,18 +361,77 @@ export class Launcher extends Observable implements DefaultLauncher { Socket.PresenceEvents.JOINED_ROOM, this.onParticipantJoinedIOC, ); + + this.LaucherRealtimeRoom.presence.on( + Socket.PresenceEvents.LEAVE, + this.onParticipantLeaveIOC, + ); + + this.LaucherRealtimeRoom.presence.on( + Socket.PresenceEvents.UPDATE, + this.onParticipantUpdatedIOC, + ); }; - private onParticipantJoinedIOC = (presence: Socket.PresenceEvent) => { - if (presence.id !== this.participant.value.id) return; + /** + * @function onParticipantJoinedIOC + * @description on participant joined + * @param presence - participant presence + * @returns {void} + */ + private onParticipantJoinedIOC = (presence: Socket.PresenceEvent): void => { + if (presence.id === this.participant.value.id) { + // Assign a slot to the participant + const _ = new SlotService(this.LaucherRealtimeRoom, this.participant.value); + this.LaucherRealtimeRoom.presence.update(this.participant.value); + } + + this.participants.value.set(presence.id, presence.data); + }; - this.onLocalParticipantJoined(); + /** + * @function onParticipantLeaveIOC + * @description on participant leave + * @param presence - participant presence + * @returns {void} + */ + private onParticipantLeaveIOC = (presence: Socket.PresenceEvent): void => { + this.participants.value.delete(presence.id); }; - private onLocalParticipantJoined = () => { - // Assign a slot to the participant - const _ = new SlotService(this.LaucherRealtimeRoom, this.participant.value); - this.LaucherRealtimeRoom.presence.update(this.participant.value); + /** + * @function onParticipantUpdatedIOC + * @description on participant updated + * @param presence - participant presence + * @returns {void} + */ + private onParticipantUpdatedIOC = (presence: Socket.PresenceEvent): void => { + if ( + presence.id === this.participant.value.id && + !isEqual(this.participant.value, presence.data) + ) { + 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 (!presence.data.activeComponents) return true; + + return this.activeComponents.includes(component.name); + }); + + this.participant.value = presence.data; + this.publish(ParticipantEvent.LOCAL_UPDATED, presence.data); + + this.logger.log('Publishing ParticipantEvent.UPDATED', presence.data); + } + + this.participants.value.set(presence.id, presence.data); + const participantList = Array.from(this.participants.value.values()); + + this.logger.log('Publishing ParticipantEvent.LIST_UPDATED', this.participants.value); + this.publish(ParticipantEvent.LIST_UPDATED, participantList); }; } diff --git a/src/services/stores/global/index.ts b/src/services/stores/global/index.ts index 66009181..7b1306c0 100644 --- a/src/services/stores/global/index.ts +++ b/src/services/stores/global/index.ts @@ -7,7 +7,7 @@ const instance: Singleton = CreateSingleton(); export class GlobalStore { public localParticipant = subject(null, true); - public participants = subject([]); + public participants = subject>(new Map()); public group = subject(null); constructor() { From c629e2ca8d37efcc9f46cec71438fad324f1c627 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 11 Mar 2024 14:08:30 -0300 Subject: [PATCH 25/83] feat: perform slot assignment on old io too --- src/core/launcher/index.ts | 2 +- src/services/realtime/ably/index.ts | 94 ----------------------------- src/services/slot/index.test.ts | 8 +-- src/services/slot/index.ts | 34 ++++++++++- 4 files changed, 37 insertions(+), 101 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 90adbdab..f188ed72 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -382,7 +382,7 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoinedIOC = (presence: Socket.PresenceEvent): void => { if (presence.id === this.participant.value.id) { // Assign a slot to the participant - const _ = new SlotService(this.LaucherRealtimeRoom, this.participant.value); + SlotService.register(this.LaucherRealtimeRoom, this.realtime, this.participant.value); this.LaucherRealtimeRoom.presence.update(this.participant.value); } diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 5a6c8a55..6cdb0350 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -830,97 +830,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably } } - /** - * @function findSlotIndex - * @description Finds an available slot index for the participant and confirms it. - * @returns {void} - */ - private findSlotIndex = async (): Promise => { - const slot = Math.floor(Math.random() * 16); - - const hasAnyOneUsingMySlot = await new Promise((resolve) => { - this.supervizChannel.presence.get((error, presences) => { - if (error) { - resolve(true); - return; - } - - presences.forEach((presence) => { - if (presence.clientId === this.myParticipant.clientId) return; - - if (presence.data.slotIndex === slot) resolve(true); - }); - - resolve(false); - }); - }); - - if (hasAnyOneUsingMySlot) { - this.logger.log( - 'slot already taken by someone else, trying again', - this.myParticipant.clientId, - ); - this.findSlotIndex(); - return; - } - - this.updateMyProperties({ slotIndex: slot }); - }; - - /** - * @function validateSlots - * @description Validates the slot index of all participants and resolves conflicts. - * @returns {void} - */ - private async validateSlots(): Promise { - const slots = []; - await new Promise((resolve) => { - this.supervizChannel.presence.get((_, presences) => { - presences.forEach((presence) => { - const hasValidSlot = - presence.data.slotIndex !== undefined && presence.data.slotIndex !== null; - - if (hasValidSlot) { - slots.push({ - slotIndex: presence.data.slotIndex, - clientId: presence.clientId, - timestamp: presence.timestamp, - }); - } - }); - resolve(true); - }); - }); - - const duplicatesMap: Record< - string, - { - slotIndex: number; - clientId: string; - timestamp: number; - }[] - > = {}; - - slots.forEach((a) => { - if (!duplicatesMap[a.slotIndex]) { - duplicatesMap[a.slotIndex] = []; - } - - duplicatesMap[a.slotIndex].push(a); - }); - - Object.values(duplicatesMap).forEach((arr) => { - const ordered = arr.sort((a, b) => a.timestamp - b.timestamp); - ordered.shift(); - - ordered.forEach((slot) => { - if (slot.clientId !== this.myParticipant.clientId) return; - - this.findSlotIndex(); - }); - }); - } - /** * @function onStateChange * @description Translates connection state and channel state into realtime state @@ -1011,8 +920,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.localRoomProperties = await this.fetchRoomProperties(); this.myParticipant = myPresence; - if (this.enableSync) this.findSlotIndex(); - if (!this.localRoomProperties) { this.initializeRoomProperties(); } else { @@ -1037,7 +944,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.updateParticipants(); this.participantJoinedObserver.publish(presence); this.updateMyProperties(); // send a sync - this.validateSlots(); }; /** diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 1c8ef002..f0a21b4a 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -16,7 +16,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, participant); + const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); @@ -47,7 +47,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, participant); + const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); @@ -80,7 +80,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, participant); + const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeUndefined(); @@ -114,7 +114,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, participant); + const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 3a36d549..0b50794e 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -6,20 +6,37 @@ import { MeetingColorsHex, } from '../../common/types/meeting-colors.types'; import { Participant } from '../../common/types/participant.types'; +import { AblyRealtimeService } from '../realtime'; export class SlotService { private room: Socket.Room; private participant: Participant; private slotIndex: number; + private realtime: AblyRealtimeService; + private static instance: SlotService; - constructor(room: Socket.Room, participant: Participant) { + // @NOTE - reciving old realtime service instance until we migrate to new IO + constructor(room: Socket.Room, realtime: AblyRealtimeService, participant: Participant) { this.room = room; this.participant = participant; + this.realtime = realtime; this.assignSlot(); this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); } + public static register( + room: Socket.Room, + realtime: AblyRealtimeService, + participant: Participant, + ) { + if (!SlotService.instance) { + SlotService.instance = new SlotService(room, realtime, participant); + } + + return SlotService.instance; + } + /** * @function assignSlot * @description Assigns a slot to the participant @@ -46,7 +63,7 @@ export class SlotService { resolve(false); }); }) - .then((isUsing) => { + .then(async (isUsing) => { if (isUsing) { this.assignSlot(); return; @@ -69,6 +86,19 @@ export class SlotService { this.room.presence.update({ slot: slotData, }); + + // @NOTE - this is a temporary fix for the issue where the slot is not being updated in the presence + // @TODO - remove this once we remove the colors from the old io + if (!this.realtime.isJoinedRoom) { + await new Promise((resolve) => { + setTimeout(resolve, 1500); + }); + } + + this.realtime.updateMyProperties({ + slotIndex: slot, + slot: slotData, + }); }) .catch((error) => { this.room.presence.update({ From 29373b753433e99641c466eca9826b0548fb5672 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 11 Mar 2024 15:25:08 -0300 Subject: [PATCH 26/83] feat: participant events from new io --- src/core/launcher/index.test.ts | 106 ++++---------------------------- src/core/launcher/index.ts | 63 ++++++++----------- 2 files changed, 39 insertions(+), 130 deletions(-) diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 30112163..7850e826 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -164,7 +164,13 @@ describe('Launcher', () => { const callback = jest.fn(); LauncherInstance.subscribe(ParticipantEvent.JOINED, callback); - LauncherInstance['onParticipantJoined'](MOCK_ABLY_PARTICIPANT); + LauncherInstance['onParticipantJoinedIOC']({ + connectionId: 'connection1', + data: MOCK_LOCAL_PARTICIPANT, + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + timestamp: Date.now(), + }); expect(spy).toHaveBeenCalledWith(ParticipantEvent.JOINED, callback); expect(LauncherInstance['publish']).toHaveBeenCalledTimes(2); @@ -178,104 +184,18 @@ describe('Launcher', () => { LauncherInstance['publish'] = jest.fn(); LauncherInstance.subscribe(ParticipantEvent.LEFT, callback); - const participant = { - clientId: MOCK_ABLY_PARTICIPANT.clientId, - action: 'absent', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - participantId: 'participant1', - }, - }; - - LauncherInstance['onParticipantLeave'](participant as AblyParticipant); - - expect(spy).toHaveBeenCalledWith(ParticipantEvent.LEFT, callback); - expect(LauncherInstance['publish']).toHaveBeenCalled(); - }); - - test('should skip and not publish ParticipantEvent.LEFT event', () => { - const callback = jest.fn(); - const spy = jest.spyOn(LauncherInstance, 'subscribe'); - LauncherInstance['publish'] = jest.fn(); - LauncherInstance.subscribe(ParticipantEvent.LEFT, callback); - - const participant = { - clientId: 'client1', - action: 'absent', + LauncherInstance['onParticipantLeaveIOC']({ connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - id: 'participant1', - participantId: 'participant1', - }, - }; - - LauncherInstance['onParticipantLeave'](participant as AblyParticipant); + data: MOCK_LOCAL_PARTICIPANT, + id: MOCK_LOCAL_PARTICIPANT.id, + name: MOCK_LOCAL_PARTICIPANT.name as string, + timestamp: Date.now(), + }); expect(spy).toHaveBeenCalledWith(ParticipantEvent.LEFT, callback); - expect(LauncherInstance['publish']).not.toHaveBeenCalled(); - }); - - test('should publish ParticipantEvent.LOCAL_LEFT event', () => { - LauncherInstance['participants'].value = new Map(); - LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); - const callback = jest.fn(); - const spy = jest.spyOn(LauncherInstance, 'subscribe'); - LauncherInstance['publish'] = jest.fn(); - LauncherInstance.subscribe(ParticipantEvent.LOCAL_LEFT, callback); - - const participant = { - clientId: MOCK_LOCAL_PARTICIPANT.id, - action: 'absent', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - id: MOCK_LOCAL_PARTICIPANT.id, - participantId: MOCK_LOCAL_PARTICIPANT.id, - }, - }; - - LauncherInstance['onParticipantLeave'](participant as AblyParticipant); - - expect(spy).toHaveBeenCalledWith(ParticipantEvent.LOCAL_LEFT, callback); expect(LauncherInstance['publish']).toHaveBeenCalled(); }); - test('should skip and publish ParticipantEvent.JOINED event', () => { - LauncherInstance['participants'].value = new Map(); - LauncherInstance['participants'].value.set(MOCK_LOCAL_PARTICIPANT.id, MOCK_LOCAL_PARTICIPANT); - - const callback = jest.fn(); - const spy = jest.spyOn(LauncherInstance, 'subscribe'); - LauncherInstance['publish'] = jest.fn(); - LauncherInstance.subscribe(ParticipantEvent.JOINED, callback); - - const participant = { - clientId: 'client1', - action: 'absent', - connectionId: 'connection1', - encoding: 'h264', - id: 'unit-test-participant-ably-id', - timestamp: new Date().getTime(), - data: { - id: 'participant1', - participantId: 'participant1', - }, - }; - - LauncherInstance['onParticipantJoined'](participant as AblyParticipant); - - expect(spy).toHaveBeenCalledWith(ParticipantEvent.JOINED, callback); - expect(LauncherInstance['publish']).not.toHaveBeenCalled(); - }); - test('should update activeComponentsInstances when participant list is updated', () => { LauncherInstance.addComponent(MOCK_COMPONENT); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index f188ed72..ae759cbf 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -175,7 +175,6 @@ export class Launcher extends Observable implements DefaultLauncher { this.realtime.authenticationObserver.unsubscribe(this.onAuthentication); this.realtime.sameAccountObserver.unsubscribe(this.onSameAccount); this.realtime.participantJoinedObserver.unsubscribe(this.onParticipantJoined); - this.realtime.participantLeaveObserver.unsubscribe(this.onParticipantLeave); this.realtime.participantsObserver.unsubscribe(this.onParticipantListUpdate); this.realtime.leave(); this.realtime = undefined; @@ -253,7 +252,6 @@ export class Launcher extends Observable implements DefaultLauncher { this.realtime.authenticationObserver.subscribe(this.onAuthentication); this.realtime.sameAccountObserver.subscribe(this.onSameAccount); this.realtime.participantJoinedObserver.subscribe(this.onParticipantJoined); - this.realtime.participantLeaveObserver.subscribe(this.onParticipantLeave); this.realtime.participantsObserver.subscribe(this.onParticipantListUpdate); }; @@ -304,41 +302,10 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {void} */ private onParticipantJoined = (ablyParticipant: AblyParticipant): void => { - this.logger.log('launcher service @ onParticipantJoined'); + if (ablyParticipant.clientId !== this.participant.value.id) return; - const participant = this.participants.value.get(ablyParticipant.clientId); - - if (!participant) return; - - if (participant.id === this.participant.value.id) { - this.logger.log('launcher service @ onParticipantJoined - local participant joined'); - this.publish(ParticipantEvent.LOCAL_JOINED, participant); - this.attachComponentsAfterJoin(); - } - - this.logger.log('launcher service @ onParticipantJoined - participant joined', participant); - this.publish(ParticipantEvent.JOINED, participant); - }; - - /** - * @function onParticipantLeave - * @description on participant leave - * @param ablyParticipant - ably participant - * @returns {void} - */ - private onParticipantLeave = (ablyParticipant: AblyParticipant): void => { - this.logger.log('launcher service @ onParticipantLeave'); - const participant = this.participants.value.get(ablyParticipant.clientId); - - if (!participant) return; - - if (participant.id === this.participant.value.id) { - this.logger.log('launcher service @ onParticipantLeave - local participant left'); - this.publish(ParticipantEvent.LOCAL_LEFT, participant); - } - - this.logger.log('launcher service @ onParticipantLeave - participant left', participant); - this.publish(ParticipantEvent.LEFT, participant); + this.logger.log('launcher service @ onParticipantJoined - local participant joined'); + this.attachComponentsAfterJoin(); }; private onSameAccount = (): void => { @@ -386,7 +353,21 @@ export class Launcher extends Observable implements DefaultLauncher { this.LaucherRealtimeRoom.presence.update(this.participant.value); } - this.participants.value.set(presence.id, presence.data); + // When the participant joins, it is without any data, it's updated later + this.participants.value.set(presence.id, { + id: presence.id, + name: presence.name, + ...presence.data, + }); + + if (presence.id === this.participant.value.id) { + this.logger.log('launcher service @ onParticipantJoined - local participant joined'); + this.publish(ParticipantEvent.LOCAL_JOINED, this.participant.value); + } + + this.logger.log('launcher service @ onParticipantJoined - participant joined', presence.data); + + this.publish(ParticipantEvent.JOINED, this.participants.value.get(presence.id)); }; /** @@ -397,6 +378,14 @@ export class Launcher extends Observable implements DefaultLauncher { */ private onParticipantLeaveIOC = (presence: Socket.PresenceEvent): void => { this.participants.value.delete(presence.id); + + if (presence.id === this.participant.value.id) { + this.logger.log('launcher service @ onParticipantLeave - local participant left'); + this.publish(ParticipantEvent.LOCAL_LEFT, presence.data); + } + + this.logger.log('launcher service @ onParticipantLeave - participant left', presence.data); + this.publish(ParticipantEvent.LEFT, presence.data); }; /** From 3c6b8220999198fe9a78a143b4895ba3ec19cade Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 11 Mar 2024 16:55:38 -0300 Subject: [PATCH 27/83] feat: destroy io instance and remove all listeners --- __mocks__/io.mock.ts | 5 +++++ src/core/launcher/index.test.ts | 8 ++------ src/core/launcher/index.ts | 7 ++++++- src/services/io/index.ts | 1 - 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/__mocks__/io.mock.ts b/__mocks__/io.mock.ts index ce0b63bf..eaf00978 100644 --- a/__mocks__/io.mock.ts +++ b/__mocks__/io.mock.ts @@ -1,4 +1,5 @@ import { jest } from '@jest/globals'; +import * as Socket from '@superviz/socket-client'; export const MOCK_IO = { Realtime: class { @@ -19,6 +20,10 @@ export const MOCK_IO = { on: jest.fn(), off: jest.fn(), emit: jest.fn(), + presence: { + on: jest.fn(), + off: jest.fn(), + }, }; } diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 7850e826..e1126b4a 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -1,10 +1,6 @@ import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; -import { - MOCK_ABLY_PARTICIPANT, - MOCK_GROUP, - MOCK_LOCAL_PARTICIPANT, -} from '../../../__mocks__/participants.mock'; +import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types'; import { Participant } from '../../common/types/participant.types'; @@ -145,7 +141,7 @@ describe('Launcher', () => { expect(MOCK_COMPONENT.attach).toHaveBeenCalledTimes(1); }); - test('should show a console message if the laucher is destroyed', () => { + test('should show a console message if the launcer is destroyed', () => { LauncherInstance.destroy(); LauncherInstance.addComponent(MOCK_COMPONENT); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index ae759cbf..8a7636af 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -15,7 +15,6 @@ import { IOC } from '../../services/io'; import LimitsService from '../../services/limits'; import { AblyRealtimeService } from '../../services/realtime'; import { AblyParticipant } from '../../services/realtime/ably/types'; -import { ParticipantInfo } from '../../services/realtime/base/types'; import { SlotService } from '../../services/slot'; import { useGlobalStore } from '../../services/stores'; import { PublicSubject } from '../../services/stores/common/types'; @@ -172,6 +171,12 @@ export class Launcher extends Observable implements DefaultLauncher { this.eventBus.destroy(); this.eventBus = undefined; + this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.JOINED_ROOM); + this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.LEAVE); + this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.UPDATE); + + this.ioc.destroy(); + this.realtime.authenticationObserver.unsubscribe(this.onAuthentication); this.realtime.sameAccountObserver.unsubscribe(this.onSameAccount); this.realtime.participantJoinedObserver.unsubscribe(this.onParticipantJoined); diff --git a/src/services/io/index.ts b/src/services/io/index.ts index f85cddea..addbc116 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -27,7 +27,6 @@ export class IOC { public destroy(): void { this.client.destroy(); this.client.connection.off(); - this.client.connection.on((state) => {}); } /** From bdbea44d5cfbfce6e0b705a673184dd1b18b42d7 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 13 Mar 2024 10:01:10 -0300 Subject: [PATCH 28/83] feat: remove client sync from ably --- __mocks__/realtime.mock.ts | 8 - src/services/pubsub/index.test.ts | 104 ------------ src/services/pubsub/index.ts | 113 ------------- src/services/pubsub/types.ts | 0 src/services/realtime/ably/index.test.ts | 192 ----------------------- src/services/realtime/ably/index.ts | 115 -------------- src/services/realtime/base/index.test.ts | 1 - src/services/realtime/base/index.ts | 2 - src/services/realtime/base/types.ts | 1 - 9 files changed, 536 deletions(-) delete mode 100644 src/services/pubsub/index.test.ts delete mode 100644 src/services/pubsub/index.ts delete mode 100644 src/services/pubsub/types.ts diff --git a/__mocks__/realtime.mock.ts b/__mocks__/realtime.mock.ts index e00deafa..3da803f0 100644 --- a/__mocks__/realtime.mock.ts +++ b/__mocks__/realtime.mock.ts @@ -39,13 +39,6 @@ export const ABLY_REALTIME_MOCK: AblyRealtimeService = { setGatherWIOParticipant: jest.fn(), domainWhitelisted: true, isDomainWhitelisted: jest.fn().mockReturnValue(true), - fetchSyncClientProperty: jest.fn((key?: string) => { - if (key) { - return createRealtimeMessage(key); - } - - return createRealtimeHistory(); - }), getSlotColor: jest.fn().mockReturnValue({ color: MeetingColorsHex[0], name: MeetingColors[0], @@ -61,7 +54,6 @@ export const ABLY_REALTIME_MOCK: AblyRealtimeService = { participantsObserver: MOCK_OBSERVER_HELPER, participantJoinedObserver: MOCK_OBSERVER_HELPER, participantLeaveObserver: MOCK_OBSERVER_HELPER, - syncPropertiesObserver: MOCK_OBSERVER_HELPER, kickAllParticipantsObserver: MOCK_OBSERVER_HELPER, kickParticipantObserver: MOCK_OBSERVER_HELPER, authenticationObserver: MOCK_OBSERVER_HELPER, diff --git a/src/services/pubsub/index.test.ts b/src/services/pubsub/index.test.ts deleted file mode 100644 index 05050ae0..00000000 --- a/src/services/pubsub/index.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; -import { - ABLY_REALTIME_MOCK, - createRealtimeHistory, - createRealtimeMessage, -} from '../../../__mocks__/realtime.mock'; -import { AblyRealtimeService } from '../realtime'; - -import { PubSub } from '.'; - -jest.mock('../realtime', () => ({ - AblyRealtimeService: jest.fn().mockImplementation(() => ABLY_REALTIME_MOCK), -})); - -describe('PubSub', () => { - let PubSubInstance: PubSub; - - beforeEach(() => { - jest.clearAllMocks(); - const realtime = new AblyRealtimeService('apiUrl', 'ablyKey'); - - PubSubInstance = new PubSub(realtime); - }); - - test('should be defined', () => { - expect(PubSub).toBeDefined(); - }); - - test('should be subscribe to event', () => { - const callback = jest.fn(); - - PubSubInstance.subscribe('test', callback); - - expect(PubSubInstance['observers'].get('test')).toBeDefined(); - }); - - test('should be unsubscribe from event', () => { - const callback = jest.fn(); - - PubSubInstance.subscribe('test', callback); - PubSubInstance.unsubscribe('test', callback); - - expect(PubSubInstance['observers'].get('test')).not.toBeDefined(); - }); - - test('should be skip unsubscribe event not exists', () => { - const callback = jest.fn(); - - PubSubInstance.unsubscribe('test', callback); - - expect(PubSubInstance['observers'].get('test')).not.toBeDefined(); - }); - - test('should be publish event to realtime', () => { - PubSubInstance.publish('test', 'test'); - - expect(PubSubInstance['realtime'].setSyncProperty).toHaveBeenCalledWith('test', 'test'); - }); - - test('should be publish event to client', () => { - const callback = jest.fn(); - - PubSubInstance.subscribe('test', callback); - PubSubInstance.publish('test', 'test'); - PubSubInstance['onSyncPropertiesChange']({ test: 'test' }); - - expect(callback).toHaveBeenCalledWith('test'); - }); - - test('should be skip publish event to client if event not exists', () => { - const callback = jest.fn(); - - PubSubInstance.subscribe('test', callback); - PubSubInstance.publish('test', 'test'); - PubSubInstance['onSyncPropertiesChange']({ test1: 'test' }); - - expect(callback).not.toHaveBeenCalled(); - }); - - test('should call the fetchHistory and return the last message of type test', () => { - jest.spyOn(PubSubInstance, 'fetchHistory'); - const history = PubSubInstance.fetchHistory('test'); - - expect(PubSubInstance.fetchHistory).toBeCalledWith('test'); - expect(ABLY_REALTIME_MOCK.fetchSyncClientProperty).toBeCalledWith('test'); - expect(history).toEqual(createRealtimeMessage('test')); - }); - - test('should call the fetchHistory and return the realtime history', () => { - jest.spyOn(PubSubInstance, 'fetchHistory'); - const history = PubSubInstance.fetchHistory(); - - expect(PubSubInstance.fetchHistory).toBeCalled(); - expect(ABLY_REALTIME_MOCK.fetchSyncClientProperty).toBeCalled(); - expect(history).toEqual(createRealtimeHistory()); - }); - - test('should destroy service', () => { - PubSubInstance.destroy(); - - expect(PubSubInstance['observers']).toEqual(new Map()); - expect(ABLY_REALTIME_MOCK.syncPropertiesObserver.unsubscribe).toHaveBeenCalled(); - }); -}); diff --git a/src/services/pubsub/index.ts b/src/services/pubsub/index.ts deleted file mode 100644 index 29575828..00000000 --- a/src/services/pubsub/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Logger, Observer } from '../../common/utils'; -import { AblyRealtimeService } from '../realtime'; -import { RealtimeMessage } from '../realtime/ably/types'; - -export class PubSub { - private readonly realtime: AblyRealtimeService; - private readonly logger: Logger; - - private observers: Map = new Map(); - - constructor(realtime: AblyRealtimeService) { - this.logger = new Logger('@superviz/sdk/pubsub'); - this.realtime = realtime; - - this.realtime.syncPropertiesObserver.subscribe(this.onSyncPropertiesChange); - - this.logger.log('PubSub created'); - } - - /** - * @function subscribe - * @description subscribe to event - * @param event - event name - * @param callback - callback function - * @returns {void} - */ - public subscribe = (event: string, callback: (data: unknown) => void): void => { - this.logger.log('pubsub service @ subscribe', { event, callback }); - - if (!this.observers.has(event)) { - this.observers.set(event, new Observer()); - } - - this.observers.get(event).subscribe(callback); - }; - - /** - * @function unsubscribe - * @description - unsubscribe from event - * @param event - event name - * @param callback - callback function - * @returns {void} - */ - public unsubscribe = (event: string, callback: (data: unknown) => void): void => { - this.logger.log('pubsub service @ unsubscribe', { event, callback }); - - if (!this.observers.has(event)) return; - - this.observers.get(event).reset(); - this.observers.delete(event); - }; - - /** - * @function publish - * @description - publish event to realtime - * @param event - event name - * @param data - data to publish - * @returns {void} - */ - public publish = (event: string, data: unknown): void => { - this.logger.log('pubsub service @ publish', { event, data }); - - this.realtime.setSyncProperty(event, data); - }; - - /** - * @function fetchSyncClientProperty - * @description get realtime client data history - * @returns {RealtimeMessage | Record} - */ - public fetchHistory( - eventName?: string, - ): Promise> { - return this.realtime.fetchSyncClientProperty(eventName); - } - - /** - * @function publishEventToClient - * @description - publish event to client - * @param event - event name - * @param data - data to publish - * @returns {void} - */ - public publishEventToClient = (event: string, data?: unknown): void => { - this.logger.log('pubsub service @ publishEventToClient', { event, data }); - - if (!this.observers.has(event)) return; - - this.observers.get(event).publish(data); - }; - - public destroy = (): void => { - this.logger.log('pubsub service @ destroy'); - - this.realtime.syncPropertiesObserver.unsubscribe(this.onSyncPropertiesChange); - this.observers.forEach((observer) => observer.destroy()); - this.observers.clear(); - }; - - /** - * @function onSyncPropertiesChange - * @description - sync properties change handler - * @param properties - properties - * @returns {void} - */ - private onSyncPropertiesChange = (properties: Record): void => { - this.logger.log('pubsub service @ onSyncPropertiesChange', { properties }); - - Object.entries(properties).forEach(([key, value]) => { - this.publishEventToClient(key, value); - }); - }; -} diff --git a/src/services/pubsub/types.ts b/src/services/pubsub/types.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/services/realtime/ably/index.test.ts b/src/services/realtime/ably/index.test.ts index 40719023..8a58e95f 100644 --- a/src/services/realtime/ably/index.test.ts +++ b/src/services/realtime/ably/index.test.ts @@ -25,27 +25,6 @@ const mockTokenRequest: Ably.Types.TokenRequest = { timestamp: new Date().getTime(), }; -const AblyClientRoomStateHistoryMock = { - items: [ - { - data: { - fizz: { - name: 'fizz', - data: "999 is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", - participantId: '123', - timestamp: new Date().getTime(), - }, - buzz: { - name: 'buzz', - data: "999 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", - participantId: '123', - timestamp: new Date().getTime(), - }, - }, - }, - ], -}; - const AblyRealtimeMock = { channels: { get: jest.fn().mockImplementation(() => { @@ -810,176 +789,6 @@ describe('AblyRealtimeService', () => { }); }); - describe('client message handlers', () => { - beforeEach(() => { - AblyRealtimeServiceInstance.start({ - apiKey: 'unit-test-api-key', - participant: MOCK_LOCAL_PARTICIPANT, - roomId: 'unit-test-room-id', - }); - - AblyRealtimeServiceInstance.join(); - - AblyRealtimeServiceInstance['state'] = RealtimeStateTypes.CONNECTED; - }); - - /** - * IsMessageTooBig - */ - - test('should return true if message size is bigger than 60kb', () => { - const message = { - data: 'a'.repeat(60000), - }; - - expect(AblyRealtimeServiceInstance['isMessageTooBig'](message)).toBeTruthy(); - }); - - test('should return false if message size is smaller than 60kb', () => { - const message = { - data: 'a'.repeat(60000 - 11), - }; - - expect(AblyRealtimeServiceInstance['isMessageTooBig'](message)).toBeFalsy(); - }); - - /** - * setSyncProperty - */ - - test('should add the event to the queue', () => { - const publishClientSyncPropertiesSpy = jest.spyOn( - AblyRealtimeServiceInstance as any, - 'publishClientSyncProperties', - ); - const name = 'test'; - const property = { test: true }; - - AblyRealtimeServiceInstance.setSyncProperty(name, property); - - expect(publishClientSyncPropertiesSpy).not.toHaveBeenCalled(); - expect(AblyRealtimeServiceInstance['clientSyncPropertiesQueue'][name]).toHaveLength(1); - }); - - test('should throw an error if the message is too big', () => { - const publishClientSyncPropertiesSpy = jest.spyOn( - AblyRealtimeServiceInstance as any, - 'publishClientSyncProperties', - ); - const throwSpy = jest.spyOn(AblyRealtimeServiceInstance as any, 'throw'); - const name = 'test'; - const property = { test: true, tooBig: new Array(10000).fill('a').join('') }; - - expect(() => AblyRealtimeServiceInstance.setSyncProperty(name, property)).toThrowError( - 'Message too long, the message limit size is 10kb.', - ); - expect(publishClientSyncPropertiesSpy).not.toHaveBeenCalled(); - expect(throwSpy).toHaveBeenCalledWith('Message too long, the message limit size is 10kb.'); - }); - - /** - * publishClientSyncProperties - * */ - - test('should not publish if the queue is empty', () => { - AblyRealtimeServiceInstance['clientSyncPropertiesQueue'] = { - test: [], - }; - - AblyRealtimeServiceInstance['publishClientSyncProperties'](); - - expect(AblyRealtimeServiceInstance['clientSyncChannel'].publish).not.toHaveBeenCalled(); - }); - - test('should not publish if the state is different than connected', () => { - AblyRealtimeServiceInstance['clientSyncPropertiesQueue'] = { - test: [], - }; - - AblyRealtimeServiceInstance['state'] = RealtimeStateTypes.DISCONNECTED; - - AblyRealtimeServiceInstance['publishClientSyncProperties'](); - - expect(AblyRealtimeServiceInstance['clientSyncChannel'].publish).not.toHaveBeenCalled(); - }); - - test('should publish the queue', () => { - AblyRealtimeServiceInstance['clientSyncPropertiesQueue'] = { - test: [ - { - data: { test: true }, - name: 'test', - participantId: 'unit-test-participant-id', - timestamp: new Date().getTime(), - }, - ], - }; - - AblyRealtimeServiceInstance['publishClientSyncProperties'](); - - expect(AblyRealtimeServiceInstance['clientSyncChannel'].publish).toHaveBeenCalled(); - }); - - /** - * fetchClientSyncProperties - */ - - test('should return the client sync properties history', async () => { - // @ts-ignore - AblyRealtimeServiceInstance['clientRoomStateChannel'].history = jest.fn((callback) => { - (callback as any)(null, AblyClientRoomStateHistoryMock); - }); - - const expected = AblyClientRoomStateHistoryMock.items[0].data; - - await expect(AblyRealtimeServiceInstance.fetchSyncClientProperty()).resolves.toEqual( - expected, - ); - }); - - test('should return the client sync properties history for a specific key', async () => { - // @ts-ignore - AblyRealtimeServiceInstance['clientRoomStateChannel'].history = jest.fn((callback) => { - (callback as any)(null, AblyClientRoomStateHistoryMock); - }); - - const expected = AblyClientRoomStateHistoryMock.items[0].data.buzz; - - await expect(AblyRealtimeServiceInstance.fetchSyncClientProperty('buzz')).resolves.toEqual( - expected, - ); - }); - - test('should return null if the history is empty', async () => { - // @ts-ignore - AblyRealtimeServiceInstance['clientRoomStateChannel'].history = jest.fn((callback) => { - (callback as any)(null, null); - }); - - await expect(AblyRealtimeServiceInstance.fetchSyncClientProperty()).resolves.toEqual(null); - }); - - test('should return throw an error if a event is not found', async () => { - // @ts-ignore - AblyRealtimeServiceInstance['clientRoomStateChannel'].history = jest.fn((callback) => { - (callback as any)(null, AblyClientRoomStateHistoryMock); - }); - - await expect( - AblyRealtimeServiceInstance.fetchSyncClientProperty('not-found'), - ).rejects.toThrow('Event not-found not found in the history'); - }); - - test('should throw an error if ably dont responds', async () => { - // @ts-ignore - AblyRealtimeServiceInstance['clientRoomStateChannel'].history = jest.fn((callback) => { - (callback as any)('error', null); - }); - - await expect(AblyRealtimeServiceInstance.fetchSyncClientProperty()).rejects.toThrow(); - }); - }); - describe('kick participant event', () => { test('should update the kickParticipant in the room properties', async () => { const participantId = 'participant1'; @@ -1407,7 +1216,6 @@ describe('AblyRealtimeService', () => { AblyRealtimeServiceInstance.freezeSync(false); expect(AblyRealtimeServiceInstance['supervizChannel'].subscribe).toBeCalled(); - expect(AblyRealtimeServiceInstance['clientSyncChannel'].subscribe).toBeCalled(); expect(AblyRealtimeServiceInstance['isSyncFrozen']).toBe(false); }); diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 6cdb0350..420898d6 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -77,16 +77,11 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.onAblyPresenceUpdate = this.onAblyPresenceUpdate.bind(this); this.onAblyPresenceLeave = this.onAblyPresenceLeave.bind(this); this.onAblyRoomUpdate = this.onAblyRoomUpdate.bind(this); - this.onClientSyncChannelUpdate = this.onClientSyncChannelUpdate.bind(this); this.onAblyChannelStateChange = this.onAblyChannelStateChange.bind(this); this.onAblyConnectionStateChange = this.onAblyConnectionStateChange.bind(this); this.onReceiveBroadcastSync = this.onReceiveBroadcastSync.bind(this); this.getParticipantSlot = this.getParticipantSlot.bind(this); this.auth = this.auth.bind(this); - - setInterval(() => { - this.publishClientSyncProperties(); - }, 1000); } public get roomProperties() { @@ -200,7 +195,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably // join custom sync channel this.clientSyncChannel = this.client.channels.get(`${this.roomId}:client-sync`); - this.clientSyncChannel.subscribe(this.onClientSyncChannelUpdate); this.clientRoomStateChannel = this.client.channels.get(`${this.roomId}:client-state`); @@ -414,36 +408,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.updatePresence3D(this.myParticipant.data); }; - /** - * @function publishClientSyncProperties - * @description publish client sync props - * @returns {void} - */ - private publishClientSyncProperties = (): void => { - if (this.state !== RealtimeStateTypes.CONNECTED) return; - - Object.keys(this.clientSyncPropertiesQueue).forEach((name) => { - this.clientSyncPropertiesQueue[name] = this.clientSyncPropertiesQueue[name].sort( - (a, b) => a.timestamp - b.timestamp, - ); - - if (!this.clientSyncPropertiesQueue[name].length) return; - - const { data, lengthToBeSplitted } = this.spliceArrayBySize( - this.clientSyncPropertiesQueue[name], - ); - - const eventQueue = data; - this.clientSyncPropertiesQueue[name].splice(0, lengthToBeSplitted); - - this.clientSyncChannel.publish(name, eventQueue, (error) => { - if (!error) return; - - this.logger.log('REALTIME', 'Error in publish client sync properties', error.message); - }); - }); - }; - /** * @function onAblyPresenceEnter * @description callback that receives the event that a participant has entered the room @@ -497,44 +461,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.onParticipantLeave(presenceMessage); } - /** - * @function onClientSyncChannelUpdate - * @description callback that receives the update event from ably's channel - * @param {Ably.Types.Message} message - * @returns {void} - */ - private onClientSyncChannelUpdate(message: Ably.Types.Message): void { - const { name, data } = message; - const property = {}; - - property[name] = data; - this.syncPropertiesObserver.publish(property); - - if (message.clientId === this.myParticipant.data.participantId) { - this.saveClientRoomState(name, data); - } - } - - /** - * @function saveClientRoomState - * @description - Saves the latest state of the room for the client - and publishes it to the client room state channel. - * @param {string} name - The name of the room state to save. - * @param {RealtimeMessage[]} data - The data to save as the latest state of the room. - * @returns {void} - */ - private saveClientRoomState = async (name: string, data: RealtimeMessage[]): Promise => { - const previusHistory = await this.fetchSyncClientProperty(); - - this.clientRoomState = Object.assign({}, previusHistory, { - [name]: data[data.length - 1], - }); - this.clientRoomStateChannel.publish('update', this.clientRoomState); - - this.logger.log('REALTIME', 'setting new room state backup', this.clientRoomState); - }; - /** * @function onReceiveBroadcastSync * @description receive the info of all participants from the host @@ -789,47 +715,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably }); } - /** - * @function fetchSyncClientProperty - * @description - * @param {string} eventName - name event to be fetched - * @returns {Promise} - */ - public async fetchSyncClientProperty( - eventName?: string, - ): Promise> { - try { - const clientHistory: Record = await new Promise( - (resolve, reject) => { - this.clientRoomStateChannel.history((error, resultPage) => { - if (error) reject(error); - - const lastMessage = resultPage?.items[0]?.data; - - if (lastMessage) { - resolve(lastMessage); - } else { - resolve(null); - } - }); - }, - ); - - if (eventName && !clientHistory[eventName]) { - throw new Error(`Event ${eventName} not found in the history`); - } - - if (eventName) { - return clientHistory[eventName]; - } - - return clientHistory; - } catch (error) { - this.logger.log('REALTIME', 'Error in fetch client realtime data', error.message); - this.throw(error.message); - } - } - /** * @function onStateChange * @description Translates connection state and channel state into realtime state diff --git a/src/services/realtime/base/index.test.ts b/src/services/realtime/base/index.test.ts index b5d9f36c..1af5d3fd 100644 --- a/src/services/realtime/base/index.test.ts +++ b/src/services/realtime/base/index.test.ts @@ -23,7 +23,6 @@ describe('RealtimeService', () => { expect(RealtimeServiceInstance.roomInfoUpdatedObserver).toBeDefined(); expect(RealtimeServiceInstance.roomListUpdatedObserver).toBeDefined(); expect(RealtimeServiceInstance.realtimeStateObserver).toBeDefined(); - expect(RealtimeServiceInstance.syncPropertiesObserver).toBeDefined(); expect(RealtimeServiceInstance.kickAllParticipantsObserver).toBeDefined(); expect(RealtimeServiceInstance.authenticationObserver).toBeDefined(); }); diff --git a/src/services/realtime/base/index.ts b/src/services/realtime/base/index.ts index 29818a26..3ef54f05 100644 --- a/src/services/realtime/base/index.ts +++ b/src/services/realtime/base/index.ts @@ -15,7 +15,6 @@ export class RealtimeService implements DefaultRealtimeService { public roomInfoUpdatedObserver: Observer; public roomListUpdatedObserver: Observer; public realtimeStateObserver: Observer; - public syncPropertiesObserver: Observer; public kickAllParticipantsObserver: Observer; public kickParticipantObserver: Observer; public authenticationObserver: Observer; @@ -41,7 +40,6 @@ export class RealtimeService implements DefaultRealtimeService { this.participantsObserver = new Observer({ logger: this.logger }); this.participantJoinedObserver = new Observer({ logger: this.logger }); this.participantLeaveObserver = new Observer({ logger: this.logger }); - this.syncPropertiesObserver = new Observer({ logger: this.logger }); this.reconnectObserver = new Observer({ logger: this.logger }); this.sameAccountObserver = new Observer({ logger: this.logger }); diff --git a/src/services/realtime/base/types.ts b/src/services/realtime/base/types.ts index 4b04815d..601263c8 100644 --- a/src/services/realtime/base/types.ts +++ b/src/services/realtime/base/types.ts @@ -12,7 +12,6 @@ export interface DefaultRealtimeService { roomInfoUpdatedObserver: Observer; roomListUpdatedObserver: Observer; realtimeStateObserver: Observer; - syncPropertiesObserver: Observer; kickAllParticipantsObserver: Observer; kickParticipantObserver: Observer; authenticationObserver: Observer; From c003f2a1722b736e2b1c69a9537bb8c0fc8d0497 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 13 Mar 2024 10:27:59 -0300 Subject: [PATCH 29/83] feat: realtime component using new io --- src/components/realtime/index.test.ts | 252 ++++++++++++++++++-------- src/components/realtime/index.ts | 111 ++++++++++-- src/components/realtime/types.ts | 5 + 3 files changed, 275 insertions(+), 93 deletions(-) diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index 8ffc4ae7..a0485908 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -1,22 +1,16 @@ +import type * as Socket from '@superviz/socket-client'; + import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; +import { MOCK_OBSERVER_HELPER } from '../../../__mocks__/observer-helper.mock'; +import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { useStore } from '../../common/utils/use-store'; +import { IOC } from '../../services/io'; -import { Realtime } from '.'; - -const PUB_SUB_MOCK = { - destroy: jest.fn(), - subscribe: jest.fn(), - unsubscribe: jest.fn(), - publish: jest.fn(), - fetchHistory: jest.fn(), - publishEventToClient: jest.fn(), -}; +import { RealtimeComponentState } from './types'; -jest.mock('../../services/pubsub', () => ({ - PubSub: jest.fn().mockImplementation(() => PUB_SUB_MOCK), -})); +import { Realtime } from '.'; jest.useFakeTimers(); @@ -26,109 +20,217 @@ describe('realtime component', () => { beforeEach(() => { jest.clearAllMocks(); + console.error = jest.fn(); + console.debug = jest.fn(); + RealtimeComponentInstance = new Realtime(); RealtimeComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, useStore, }); - }); - - test('should be subscribe to event', () => { - const callback = jest.fn(); - RealtimeComponentInstance.subscribe('test', callback); + RealtimeComponentInstance['state'] = RealtimeComponentState.STARTED; + }); - expect(PUB_SUB_MOCK.subscribe).toHaveBeenCalledWith('test', callback); + afterEach(() => { + jest.clearAllMocks(); + RealtimeComponentInstance.detach(); }); - test('should be unsubscribe from event', () => { - const callback = jest.fn(); + test('should create a new instance of Realtime', () => { + expect(RealtimeComponentInstance).toBeInstanceOf(Realtime); + }); - RealtimeComponentInstance.unsubscribe('test', callback); + describe('start', () => { + test('should subscribe to realtiem events', () => { + const spy = jest.spyOn(RealtimeComponentInstance, 'subscribeToRealtimeEvents' as any); + RealtimeComponentInstance['start'](); - expect(PUB_SUB_MOCK.unsubscribe).toHaveBeenCalledWith('test', callback); - }); + expect(spy).toHaveBeenCalled(); + }); - test('should be publish event to realtime', () => { - RealtimeComponentInstance.publish('test', 'test'); + test('should subscribe to callbacks when joined', () => { + const spy = jest.spyOn(RealtimeComponentInstance['room'], 'on' as any); + RealtimeComponentInstance['start'](); - expect(PUB_SUB_MOCK.publish).toHaveBeenCalledWith('test', 'test'); + expect(spy).toHaveBeenCalled(); + }); }); - test('should be fetch history', async () => { - RealtimeComponentInstance.fetchHistory('test'); + describe('destroy', () => { + test('should disconnect from the room', () => { + const spy = jest.spyOn(RealtimeComponentInstance['room'], 'disconnect' as any); + RealtimeComponentInstance['destroy'](); - expect(PUB_SUB_MOCK.fetchHistory).toHaveBeenCalledWith('test'); - }); + expect(spy).toHaveBeenCalled(); + }); - test('should destroy pubsub when destroy realtime component', () => { - RealtimeComponentInstance.detach(); + test('should change state to stopped', () => { + RealtimeComponentInstance['destroy'](); - expect(PUB_SUB_MOCK.destroy).toHaveBeenCalled(); + expect(RealtimeComponentInstance['state']).toBe('STOPPED'); + }); }); - test('when realtime is not joined room should store callback to subscribe', () => { - const callback = jest.fn(); - const RealtimeComponentInstance = new Realtime(); + describe('publish', () => { + test('should log an error when trying to publish an event before start', () => { + RealtimeComponentInstance['state'] = RealtimeComponentState.STOPPED; - RealtimeComponentInstance.attach({ - realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - useStore, + const spy = jest.spyOn(RealtimeComponentInstance['logger'], 'log' as any); + RealtimeComponentInstance.publish('test'); + + expect(spy).toHaveBeenCalled(); }); - RealtimeComponentInstance.subscribe('test', callback); + test('should publish an event', () => { + const spy = jest.spyOn(RealtimeComponentInstance['room'], 'emit' as any); + RealtimeComponentInstance['start'](); + RealtimeComponentInstance.publish('test'); - expect(PUB_SUB_MOCK.subscribe).not.toHaveBeenCalled(); - expect(RealtimeComponentInstance['callbacksToSubscribeWhenJoined']).toEqual([ - { event: 'test', callback }, - ]); + expect(spy).toHaveBeenCalled(); + }); }); - test('should subscribe to events when joined room', async () => { - const callback = jest.fn(); - const RealtimeComponentInstance = new Realtime(); + describe('subscribe', () => { + test('should subscribe to an event', () => { + RealtimeComponentInstance['observers']['test'] = MOCK_OBSERVER_HELPER; + const spy = jest.spyOn(RealtimeComponentInstance['observers']['test'], 'subscribe' as any); + RealtimeComponentInstance['start'](); + RealtimeComponentInstance.subscribe('test', () => {}); - RealtimeComponentInstance.attach({ - realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - useStore, + expect(spy).toHaveBeenCalled(); }); - RealtimeComponentInstance.subscribe('test', callback); + test('should subscribe to an event when joined', () => { + RealtimeComponentInstance['state'] = RealtimeComponentState.STOPPED; + RealtimeComponentInstance.subscribe('test', () => {}); - expect(PUB_SUB_MOCK.subscribe).not.toHaveBeenCalled(); + expect(RealtimeComponentInstance['callbacksToSubscribeWhenJoined']).toHaveLength(1); + }); + }); - RealtimeComponentInstance['realtime'] = Object.assign({}, ABLY_REALTIME_MOCK, { - isJoinedRoom: true, + describe('unsubscribe', () => { + test('should unsubscribe from an event', () => { + RealtimeComponentInstance['observers']['test'] = MOCK_OBSERVER_HELPER; + const spy = jest.spyOn(RealtimeComponentInstance['observers']['test'], 'unsubscribe' as any); + RealtimeComponentInstance['start'](); + RealtimeComponentInstance.unsubscribe('test', () => {}); + + expect(spy).toHaveBeenCalled(); }); + }); - // mock start call - RealtimeComponentInstance['start'](); + describe('subscribeToRealtimeEvents', () => { + test('should subscribe to all realtime events', () => { + const spy = jest.spyOn(RealtimeComponentInstance['room'], 'on' as any); + RealtimeComponentInstance['start'](); + RealtimeComponentInstance['subscribeToRealtimeEvents'](); - expect(PUB_SUB_MOCK.subscribe).toHaveBeenCalledWith('test', callback); + expect(spy).toHaveBeenCalled(); + }); }); - test('should not publish event when realtime is not started', () => { - console.error = jest.fn(); - const RealtimeComponentInstance = new Realtime(); + describe('fetchHistory', () => { + test('should return null when the history is empty', async () => { + const spy = jest + .spyOn(RealtimeComponentInstance['room'], 'history' as any) + .mockImplementationOnce((...args: unknown[]) => { + const next = args[0] as (data: any) => void; + next({ events: [] }); + }); - RealtimeComponentInstance.attach({ - realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: false }), - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - useStore, + RealtimeComponentInstance['start'](); + const h = await RealtimeComponentInstance.fetchHistory(); + + expect(spy).toHaveBeenCalled(); + expect(h).toEqual(null); }); - RealtimeComponentInstance.publish('test', 'test'); + test('should return the history', async () => { + const spy = jest + .spyOn(RealtimeComponentInstance['room'], 'history' as any) + .mockImplementationOnce((...args: unknown[]) => { + const next = args[0] as (data: any) => void; + next({ + events: [ + { + timestamp: 1710336284652, + presence: { id: 'unit-test-presence-id', name: 'unit-test-presence-name' }, + data: { name: 'unit-test-event-name', payload: 'unit-test-event-payload' }, + }, + ], + }); + }); + + RealtimeComponentInstance['start'](); + const h = await RealtimeComponentInstance.fetchHistory(); + + expect(spy).toHaveBeenCalled(); + expect(h).toEqual({ + 'unit-test-event-name': [ + { + data: 'unit-test-event-payload', + name: 'unit-test-event-name', + participantId: 'unit-test-presence-id', + timestamp: 1710336284652, + }, + ], + }); + }); - expect(console.error).toHaveBeenCalledWith( - "Realtime component is not started yet. You can't publish event test before start", - ); - expect(PUB_SUB_MOCK.publish).not.toHaveBeenCalledWith('test', 'test'); + test('should return the history for a specific event', async () => { + const spy = jest + .spyOn(RealtimeComponentInstance['room'], 'history' as any) + .mockImplementationOnce((...args: unknown[]) => { + const next = args[0] as (data: any) => void; + next({ + events: [ + { + timestamp: 1710336284652, + presence: { id: 'unit-test-presence-id', name: 'unit-test-presence-name' }, + data: { name: 'unit-test-event-name', payload: 'unit-test-event-payload' }, + }, + ], + }); + }); + + RealtimeComponentInstance['start'](); + const h = await RealtimeComponentInstance.fetchHistory('unit-test-event-name'); + + expect(spy).toHaveBeenCalled(); + expect(h).toEqual([ + { + data: 'unit-test-event-payload', + name: 'unit-test-event-name', + participantId: 'unit-test-presence-id', + timestamp: 1710336284652, + }, + ]); + }); + + test('should reject when the event is not found', async () => { + const spy = jest + .spyOn(RealtimeComponentInstance['room'], 'history' as any) + .mockImplementationOnce((...args: unknown[]) => { + const next = args[0] as (data: any) => void; + next({ + events: [ + { + timestamp: 1710336284652, + presence: { id: 'unit-test-presence-id', name: 'unit-test-presence-name' }, + data: { name: 'unit-test-event-name', payload: 'unit-test-event-payload' }, + }, + ], + }); + }); + + RealtimeComponentInstance['start'](); + const h = RealtimeComponentInstance.fetchHistory('unit-test-event-wrong-name'); + + await expect(h).rejects.toThrow('Event unit-test-event-wrong-name not found in the history'); + }); }); }); diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index 99d40829..34eb8830 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -1,14 +1,15 @@ +import * as Socket from '@superviz/socket-client'; + +import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; -import { PubSub } from '../../services/pubsub'; import { RealtimeMessage } from '../../services/realtime/ably/types'; import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; +import { Participant } from '../who-is-online/types'; -import { RealtimeComponentEvent, RealtimeComponentState } from './types'; +import { RealtimeComponentEvent, RealtimeComponentState, RealtimeData } from './types'; export class Realtime extends BaseComponent { - private pubsub: PubSub; - private callbacksToSubscribeWhenJoined: Array<{ event: string; callback: (data: unknown) => void; @@ -17,29 +18,31 @@ export class Realtime extends BaseComponent { protected logger: Logger; public name: ComponentNames; private state: RealtimeComponentState = RealtimeComponentState.STOPPED; + private declare localParticipant: Participant; constructor() { super(); this.name = ComponentNames.REALTIME; this.logger = new Logger('@superviz/sdk/realtime-component'); + const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe(); } protected destroy(): void { this.logger.log('destroyed'); this.changeState(RealtimeComponentState.STOPPED); - this.pubsub.destroy(); + this.room.disconnect(); } protected start(): void { this.logger.log('started'); - this.pubsub = new PubSub(this.realtime); this.callbacksToSubscribeWhenJoined.forEach(({ event, callback }) => { - this.pubsub.subscribe(event, callback); + this.room.on(event, callback); }); - this.changeState(RealtimeComponentState.STARTED); + this.subscribeToRealtimeEvents(); } /** @@ -50,14 +53,14 @@ export class Realtime extends BaseComponent { * @returns {void} */ public publish = (event: string, data?: unknown): void => { - if (!this.pubsub) { + if (this.state !== RealtimeComponentState.STARTED) { const message = `Realtime component is not started yet. You can't publish event ${event} before start`; this.logger.log(message); console.error(message); return; } - this.pubsub.publish(event, data); + this.room.emit('message', { name: event, payload: data }); }; /** @@ -68,12 +71,12 @@ export class Realtime extends BaseComponent { * @returns {void} */ public subscribe = (event: string, callback: (data: unknown) => void): void => { - if (!this.pubsub) { + if (this.state !== RealtimeComponentState.STARTED) { this.callbacksToSubscribeWhenJoined.push({ event, callback }); return; } - this.pubsub.subscribe(event, callback); + this.observers[event].subscribe(callback); }; /** @@ -84,18 +87,61 @@ export class Realtime extends BaseComponent { * @returns {void} */ public unsubscribe = (event: string, callback?: (data: unknown) => void): void => { - this.pubsub.unsubscribe(event, callback); + if (!this.observers[event]) return; + + this.observers[event].unsubscribe(callback); }; /** - * @function fetchSyncClientProperty + * @function fetchHistory * @description get realtime client data history * @returns {RealtimeMessage | Record} */ - public fetchHistory( + public async fetchHistory( eventName?: string, - ): Promise> { - return this.pubsub.fetchHistory(eventName); + ): Promise | null> { + const history: RealtimeMessage[] | Record = await new Promise( + (resolve, reject) => { + const next = (data: Socket.RoomHistory) => { + if (!data.events?.length) { + resolve(null); + } + + const groupMessages = data.events.reduce( + (group: Record, event: Socket.SocketEvent) => { + if (!group[event.data.name]) { + // eslint-disable-next-line no-param-reassign + group[event.data.name] = []; + } + + group[event.data.name].push({ + data: event.data.payload, + name: event.data.name, + participantId: event.presence.id, + timestamp: event.timestamp, + }); + + return group; + }, + {}, + ); + + if (eventName && !groupMessages[eventName]) { + reject(new Error(`Event ${eventName} not found in the history`)); + } + + if (eventName) { + resolve(groupMessages[eventName]); + } + + resolve(groupMessages); + }; + + this.room.history(next); + }, + ); + + return history; } /** @@ -108,6 +154,35 @@ export class Realtime extends BaseComponent { this.logger.log('realtime component @ changeState - state changed', state); this.state = state; - this.pubsub.publishEventToClient(RealtimeComponentEvent.REALTIME_STATE_CHANGED, this.state); + this.publishEventToClient(RealtimeComponentEvent.REALTIME_STATE_CHANGED, this.state); + } + + private subscribeToRealtimeEvents(): void { + this.room.presence.on(Socket.PresenceEvents.JOINED_ROOM, (event) => { + if (event.id !== this.localParticipant.id) return; + + this.logger.log('joined room'); + this.changeState(RealtimeComponentState.STARTED); + }); + + this.room.on('message', (event) => { + this.logger.log('message received', event); + this.publishEventToClient(event.data.name, event.data.payload); + }); } + + /** + * @function publishEventToClient + * @description - publish event to client + * @param event - event name + * @param data - data to publish + * @returns {void} + */ + public publishEventToClient = (event: string, data?: unknown): void => { + this.logger.log('pubsub service @ publishEventToClient', { event, data }); + + if (!this.observers[event]) return; + + this.observers[event].publish(data); + }; } diff --git a/src/components/realtime/types.ts b/src/components/realtime/types.ts index 80dc15b1..b2a9e80f 100644 --- a/src/components/realtime/types.ts +++ b/src/components/realtime/types.ts @@ -6,3 +6,8 @@ export enum RealtimeComponentState { export enum RealtimeComponentEvent { REALTIME_STATE_CHANGED = 'realtime-component.state-changed', } + +export type RealtimeData = { + name: string; + payload: any; +}; From f961d7f5a71e6320243df595aa88a08503c38522 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 13 Mar 2024 10:37:03 -0300 Subject: [PATCH 30/83] feat: inject ioc on components --- src/components/base/index.test.ts | 7 ++++++ src/components/base/index.ts | 9 +++++++- src/components/base/types.ts | 2 ++ src/components/comments/index.test.ts | 4 ++++ .../presence-mouse/canvas/index.test.ts | 2 ++ .../presence-mouse/html/index.test.ts | 2 ++ src/components/video/index.test.ts | 3 +++ src/components/who-is-online/index.test.ts | 2 ++ src/core/launcher/index.test.ts | 19 +++++++++------ src/core/launcher/index.ts | 23 ++++++++++--------- 10 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/components/base/index.test.ts b/src/components/base/index.test.ts index 038ce8a4..0ccf1d46 100644 --- a/src/components/base/index.test.ts +++ b/src/components/base/index.test.ts @@ -7,6 +7,7 @@ import { Logger } from '../../common/utils'; import { useStore } from '../../common/utils/use-store'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; +import { IOC } from '../../services/io'; import { AblyRealtimeService } from '../../services/realtime'; import { useGlobalStore } from '../../services/stores'; import { ComponentNames } from '../types'; @@ -63,6 +64,7 @@ describe('BaseComponent', () => { describe('attach', () => { test('should not call start if realtime is not joined room', () => { DummyComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -86,6 +88,7 @@ describe('BaseComponent', () => { ablyMock['isDomainWhitelisted'] = false; DummyComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: ablyMock as AblyRealtimeService, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -104,6 +107,7 @@ describe('BaseComponent', () => { expect(DummyComponentInstance.attach).toBeDefined(); DummyComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -120,6 +124,7 @@ describe('BaseComponent', () => { expect(() => { DummyComponentInstance.attach({ + ioc: null as unknown as IOC, realtime: null as unknown as AblyRealtimeService, config: null as unknown as Configuration, eventBus: null as unknown as EventBus, @@ -135,6 +140,7 @@ describe('BaseComponent', () => { expect(DummyComponentInstance.detach).toBeDefined(); DummyComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -155,6 +161,7 @@ describe('BaseComponent', () => { DummyComponentInstance['destroy'] = jest.fn(); DummyComponentInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 31bfd559..1b50943c 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -1,9 +1,12 @@ +import * as Socket from '@superviz/socket-client'; + import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { Group } from '../../common/types/participant.types'; import { Logger, Observable } from '../../common/utils'; import { useStore } from '../../common/utils/use-store'; import config from '../../services/config'; import { EventBus } from '../../services/event-bus'; +import { IOC } from '../../services/io'; import { AblyRealtimeService } from '../../services/realtime'; import { ComponentNames } from '../types'; @@ -13,11 +16,13 @@ export abstract class BaseComponent extends Observable { public abstract name: ComponentNames; protected abstract logger: Logger; protected group: Group; + protected ioc: IOC; protected realtime: AblyRealtimeService; protected eventBus: EventBus; protected isAttached = false; protected unsubscribeFrom: Array<(id: unknown) => void> = []; protected useStore = useStore.bind(this) as typeof useStore; + protected room: Socket.Room; /** * @function attach @@ -33,7 +38,7 @@ export abstract class BaseComponent extends Observable { throw new Error(message); } - const { realtime, config: globalConfig, eventBus } = params; + const { realtime, config: globalConfig, eventBus, ioc } = params; if (!realtime.isDomainWhitelisted) { const message = `Component ${this.name} can't be used because this website's domain is not whitelisted. Please add your domain in https://dashboard.superviz.com/developer`; @@ -46,6 +51,8 @@ export abstract class BaseComponent extends Observable { this.realtime = realtime; this.eventBus = eventBus; this.isAttached = true; + this.ioc = ioc; + this.room = ioc.createRoom(this.name); if (!this.realtime.isJoinedRoom) { this.logger.log(`${this.name} @ attach - not joined yet`); diff --git a/src/components/base/types.ts b/src/components/base/types.ts index 24496c4e..2840803d 100644 --- a/src/components/base/types.ts +++ b/src/components/base/types.ts @@ -1,10 +1,12 @@ import { Store, StoreType } from '../../common/types/stores.types'; import { Configuration } from '../../services/config/types'; import { EventBus } from '../../services/event-bus'; +import { IOC } from '../../services/io'; import { AblyRealtimeService } from '../../services/realtime'; import { useGlobalStore } from '../../services/stores'; export interface DefaultAttachComponentOptions { + ioc: IOC; realtime: AblyRealtimeService; config: Configuration; eventBus: EventBus; diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index 3ae9cd28..e87a06f3 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -9,6 +9,7 @@ import { ParticipantByGroupApi } from '../../common/types/participant.types'; import sleep from '../../common/utils/sleep'; import { useStore } from '../../common/utils/use-store'; import ApiService from '../../services/api'; +import { IOC } from '../../services/io'; import { useGlobalStore } from '../../services/stores'; import { CommentsFloatButton } from '../../web-components'; import { ComponentNames } from '../types'; @@ -77,6 +78,7 @@ describe('Comments', () => { commentsComponent = new Comments(DummiePinAdapter); commentsComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -330,6 +332,7 @@ describe('Comments', () => { const spy = jest.spyOn(commentsComponent['logger'], 'log'); commentsComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -348,6 +351,7 @@ describe('Comments', () => { (ApiService.fetchAnnotation as jest.Mock).mockReturnValueOnce([MOCK_ANNOTATION]); commentsComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/components/presence-mouse/canvas/index.test.ts b/src/components/presence-mouse/canvas/index.test.ts index 2bf27146..0560ed55 100644 --- a/src/components/presence-mouse/canvas/index.test.ts +++ b/src/components/presence-mouse/canvas/index.test.ts @@ -4,6 +4,7 @@ import { EVENT_BUS_MOCK } from '../../../../__mocks__/event-bus.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../../__mocks__/realtime.mock'; import { useStore } from '../../../common/utils/use-store'; +import { IOC } from '../../../services/io'; import { ParticipantMouse } from '../types'; import { PointersCanvas } from './index'; @@ -33,6 +34,7 @@ const createMousePointers = (): PointersCanvas => { const presenceMouseComponent = new PointersCanvas('canvas'); presenceMouseComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index cf758892..0f31ef5a 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -4,6 +4,7 @@ import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock' import { ABLY_REALTIME_MOCK } from '../../../../__mocks__/realtime.mock'; import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; import { useStore } from '../../../common/utils/use-store'; +import { IOC } from '../../../services/io'; import { ParticipantMouse, Element } from '../types'; import { PointersHTML } from '.'; @@ -13,6 +14,7 @@ const createMousePointers = (): PointersHTML => { presenceMouseComponent['localParticipant'] = MOCK_LOCAL_PARTICIPANT; presenceMouseComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: ABLY_REALTIME_MOCK, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index ce975a2b..0b112e88 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -16,6 +16,7 @@ import { import { MeetingColors } from '../../common/types/meeting-colors.types'; import { ParticipantType } from '../../common/types/participant.types'; import { useStore } from '../../common/utils/use-store'; +import { IOC } from '../../services/io'; import { AblyParticipant, AblyRealtimeData } from '../../services/realtime/ably/types'; import { VideoFrameState } from '../../services/video-conference-manager/types'; import { ComponentNames } from '../types'; @@ -95,6 +96,7 @@ describe('VideoConference', () => { VideoConferenceInstance['localParticipant'] = MOCK_LOCAL_PARTICIPANT; VideoConferenceInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: MOCK_REALTIME, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, @@ -119,6 +121,7 @@ describe('VideoConference', () => { }; VideoConferenceInstance.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: MOCK_REALTIME, config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index 966af98e..dff131ed 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -10,6 +10,7 @@ import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; import { useStore } from '../../common/utils/use-store'; +import { IOC } from '../../services/io'; import { WhoIsOnline } from './index'; @@ -21,6 +22,7 @@ describe('Who Is Online', () => { whoIsOnlineComponent = new WhoIsOnline(); whoIsOnlineComponent.attach({ + ioc: new IOC(MOCK_LOCAL_PARTICIPANT), realtime: Object.assign({}, ABLY_REALTIME_MOCK, { isJoinedRoom: true }), config: MOCK_CONFIG, eventBus: EVENT_BUS_MOCK, diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index e1126b4a..1ae2ba28 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -6,7 +6,9 @@ import { ParticipantEvent, RealtimeEvent } from '../../common/types/events.types import { Participant } from '../../common/types/participant.types'; import { useStore } from '../../common/utils/use-store'; import { BaseComponent } from '../../components/base'; +import { DefaultAttachComponentOptions } from '../../components/base/types'; import { ComponentNames } from '../../components/types'; +import { IOC } from '../../services/io'; import LimitsService from '../../services/limits'; import { AblyParticipant } from '../../services/realtime/ably/types'; import { useGlobalStore } from '../../services/stores'; @@ -94,12 +96,15 @@ describe('Launcher', () => { LauncherInstance.addComponent(MOCK_COMPONENT); - expect(MOCK_COMPONENT.attach).toHaveBeenCalledWith({ - realtime: ABLY_REALTIME_MOCK, - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - useStore, - }); + expect(MOCK_COMPONENT.attach).toHaveBeenCalledWith( + expect.objectContaining({ + ioc: expect.any(IOC), + realtime: ABLY_REALTIME_MOCK, + config: MOCK_CONFIG, + eventBus: EVENT_BUS_MOCK, + useStore, + } as DefaultAttachComponentOptions), + ); expect(ABLY_REALTIME_MOCK.updateMyProperties).toHaveBeenCalledWith({ activeComponents: [MOCK_COMPONENT.name], @@ -141,7 +146,7 @@ describe('Launcher', () => { expect(MOCK_COMPONENT.attach).toHaveBeenCalledTimes(1); }); - test('should show a console message if the launcer is destroyed', () => { + test('should show a console message if the launcher is destroyed', () => { LauncherInstance.destroy(); LauncherInstance.addComponent(MOCK_COMPONENT); diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 8a7636af..dc4c10cb 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -30,7 +30,7 @@ export class Launcher extends Observable implements DefaultLauncher { private activeComponentsInstances: Partial[] = []; private ioc: IOC; - private LaucherRealtimeRoom: Socket.Room; + private LauncherRealtimeRoom: Socket.Room; private realtime: AblyRealtimeService; private eventBus: EventBus = new EventBus(); @@ -62,7 +62,7 @@ export class Launcher extends Observable implements DefaultLauncher { // SuperViz IO Room this.ioc = new IOC(this.participant.value); - this.LaucherRealtimeRoom = this.ioc.createRoom('launcher'); + this.LauncherRealtimeRoom = this.ioc.createRoom('launcher'); // internal events without realtime this.eventBus = new EventBus(); @@ -89,6 +89,7 @@ export class Launcher extends Observable implements DefaultLauncher { } component.attach({ + ioc: this.ioc, realtime: this.realtime, config: config.configuration, eventBus: this.eventBus, @@ -171,9 +172,9 @@ export class Launcher extends Observable implements DefaultLauncher { this.eventBus.destroy(); this.eventBus = undefined; - this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.JOINED_ROOM); - this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.LEAVE); - this.LaucherRealtimeRoom.presence.off(Socket.PresenceEvents.UPDATE); + this.LauncherRealtimeRoom.presence.off(Socket.PresenceEvents.JOINED_ROOM); + this.LauncherRealtimeRoom.presence.off(Socket.PresenceEvents.LEAVE); + this.LauncherRealtimeRoom.presence.off(Socket.PresenceEvents.UPDATE); this.ioc.destroy(); @@ -296,7 +297,7 @@ export class Launcher extends Observable implements DefaultLauncher { }); if (localParticipant && !isEqual(this.participant.value, localParticipant)) { - this.LaucherRealtimeRoom.presence.update(localParticipant); + this.LauncherRealtimeRoom.presence.update(localParticipant); } }; @@ -329,17 +330,17 @@ export class Launcher extends Observable implements DefaultLauncher { private startIOC = (): void => { this.logger.log('launcher service @ startIOC'); - this.LaucherRealtimeRoom.presence.on( + this.LauncherRealtimeRoom.presence.on( Socket.PresenceEvents.JOINED_ROOM, this.onParticipantJoinedIOC, ); - this.LaucherRealtimeRoom.presence.on( + this.LauncherRealtimeRoom.presence.on( Socket.PresenceEvents.LEAVE, this.onParticipantLeaveIOC, ); - this.LaucherRealtimeRoom.presence.on( + this.LauncherRealtimeRoom.presence.on( Socket.PresenceEvents.UPDATE, this.onParticipantUpdatedIOC, ); @@ -354,8 +355,8 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoinedIOC = (presence: Socket.PresenceEvent): void => { if (presence.id === this.participant.value.id) { // Assign a slot to the participant - SlotService.register(this.LaucherRealtimeRoom, this.realtime, this.participant.value); - this.LaucherRealtimeRoom.presence.update(this.participant.value); + SlotService.register(this.LauncherRealtimeRoom, this.realtime, this.participant.value); + this.LauncherRealtimeRoom.presence.update(this.participant.value); } // When the participant joins, it is without any data, it's updated later From efa0d5a938b54d2908807788ae730e2ac0f53df0 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 13 Mar 2024 11:36:03 -0300 Subject: [PATCH 31/83] feat: event subscribers and events payload --- src/components/realtime/index.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index 34eb8830..b7cd8d9f 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -1,7 +1,8 @@ import * as Socket from '@superviz/socket-client'; +import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { StoreType } from '../../common/types/stores.types'; -import { Logger } from '../../common/utils'; +import { Logger, Observer } from '../../common/utils'; import { RealtimeMessage } from '../../services/realtime/ably/types'; import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; @@ -38,10 +39,6 @@ export class Realtime extends BaseComponent { protected start(): void { this.logger.log('started'); - this.callbacksToSubscribeWhenJoined.forEach(({ event, callback }) => { - this.room.on(event, callback); - }); - this.subscribeToRealtimeEvents(); } @@ -53,6 +50,11 @@ export class Realtime extends BaseComponent { * @returns {void} */ public publish = (event: string, data?: unknown): void => { + if (Object.values(ComponentLifeCycleEvent).includes(event as ComponentLifeCycleEvent)) { + this.publishEventToClient(event, data); + return; + } + if (this.state !== RealtimeComponentState.STARTED) { const message = `Realtime component is not started yet. You can't publish event ${event} before start`; this.logger.log(message); @@ -76,6 +78,10 @@ export class Realtime extends BaseComponent { return; } + if (!this.observers[event]) { + this.observers[event] = new Observer(); + } + this.observers[event].subscribe(callback); }; @@ -163,11 +169,20 @@ export class Realtime extends BaseComponent { this.logger.log('joined room'); this.changeState(RealtimeComponentState.STARTED); + + this.callbacksToSubscribeWhenJoined.forEach(({ event, callback }) => { + this.subscribe(event, callback); + }); }); this.room.on('message', (event) => { this.logger.log('message received', event); - this.publishEventToClient(event.data.name, event.data.payload); + this.publishEventToClient(event.data.name, { + data: event.data.payload, + participantId: event.presence.id, + name: event.data.name, + timestamp: event.timestamp, + } as RealtimeMessage); }); } From ef915cb945d45358da28be5585ff0f12634158c5 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 14 Mar 2024 18:07:05 -0300 Subject: [PATCH 32/83] feat: presence mouses HTML with new realtime --- .../presence-mouse/html/index.test.ts | 290 ++++++++++-------- src/components/presence-mouse/html/index.ts | 122 ++++---- src/components/presence-mouse/index.ts | 5 + 3 files changed, 229 insertions(+), 188 deletions(-) diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index fa838e25..1235244e 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -2,7 +2,6 @@ import { MOCK_CONFIG } from '../../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../../__mocks__/event-bus.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../../__mocks__/realtime.mock'; -import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; import { useStore } from '../../../common/utils/use-store'; import { IOC } from '../../../services/io'; import { ParticipantMouse } from '../types'; @@ -36,15 +35,22 @@ describe('MousePointers on HTML', () => { ...MOCK_LOCAL_PARTICIPANT, x: 30, y: 30, - slotIndex: 0, + slotIndex: 7, + slot: { + index: 7, + color: '#304AFF', + textColor: '#fff', + colorName: 'bluedark', + timestamp: 1710448079918, + }, visible: true, }; const participant1 = { ...MOCK_MOUSE }; const participant2 = { ...MOCK_MOUSE }; const participant3 = { ...MOCK_MOUSE }; - participant2.id = 'unit-test-participant2-ably-id'; - participant3.id = 'unit-test-participant3-ably-id'; + participant2.id = 'unit-test-participant2-id'; + participant3.id = 'unit-test-participant3-id'; participants[participant1.id] = { ...participant1 }; participants[participant2.id] = { ...participant2 }; @@ -77,16 +83,6 @@ describe('MousePointers on HTML', () => { jest.resetAllMocks(); }); - test('should call enterPresenceMouseChannel', () => { - const enterPresenceMouseChannelSpy = jest.spyOn( - ABLY_REALTIME_MOCK, - 'enterPresenceMouseChannel', - ); - presenceMouseComponent['start'](); - - expect(enterPresenceMouseChannelSpy).toHaveBeenCalledWith(MOCK_LOCAL_PARTICIPANT); - }); - test('should call renderWrapper', () => { const renderWrapperSpy = jest.spyOn(presenceMouseComponent as any, 'renderWrapper'); @@ -140,16 +136,6 @@ describe('MousePointers on HTML', () => { ); }); - test('should call realtime.leavePresenceMouseChannel', () => { - const leavePresenceMouseChannelSpy = jest.spyOn( - ABLY_REALTIME_MOCK, - 'leavePresenceMouseChannel', - ); - presenceMouseComponent['destroy'](); - - expect(leavePresenceMouseChannelSpy).toHaveBeenCalled(); - }); - test('should remove wrapper from the DOM', () => { const wrapperSpy = jest.spyOn(presenceMouseComponent['wrapper'] as any, 'remove'); @@ -410,10 +396,10 @@ describe('MousePointers on HTML', () => { presenceMouseComponent = createMousePointers(); }); - test('should call realtime.updatePresenceMouse', () => { + test('should call room.presence.update', () => { const updatePresenceMouseSpy = jest.spyOn( - presenceMouseComponent['realtime'], - 'updatePresenceMouse', + presenceMouseComponent['room']['presence'], + 'update', ); presenceMouseComponent['transformPointer']({ translate: { x: 10, y: 10 }, scale: 1 }); @@ -441,10 +427,10 @@ describe('MousePointers on HTML', () => { }); }); - test('should not call realtime.updatePresenceMouse if isPrivate', () => { + test('should not call room.presence.update if isPrivate', () => { const updatePresenceMouseSpy = jest.spyOn( - presenceMouseComponent['realtime'], - 'updatePresenceMouse', + presenceMouseComponent['room']['presence'], + 'update', ); presenceMouseComponent['isPrivate'] = true; @@ -466,28 +452,38 @@ describe('MousePointers on HTML', () => { }); describe('onMyParticipantMouseLeave', () => { - test('should call realtime.updatePresenceMouse', () => { + test('should call room.presence.update', () => { const updatePresenceMouseSpy = jest.spyOn( - presenceMouseComponent['realtime'], - 'updatePresenceMouse', + presenceMouseComponent['room']['presence'], + 'update', ); presenceMouseComponent['onMyParticipantMouseLeave'](); - expect(updatePresenceMouseSpy).toHaveBeenCalledWith({ - ...MOCK_LOCAL_PARTICIPANT, - visible: false, - }); + expect(updatePresenceMouseSpy).toHaveBeenCalledWith({ visible: false }); }); }); - describe('onParticipantsDidChange', () => { + describe('on participant updated', () => { test('should set presences', () => { - presenceMouseComponent['onParticipantsDidChange'](participants); - const map = new Map(Object.entries(participants)); - map.delete(MOCK_LOCAL_PARTICIPANT.id); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); + + const ex = new Map(); + ex.set('unit-test-participant2-id', { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }); - expect(presenceMouseComponent['presences']).toEqual(map); + expect(presenceMouseComponent['presences']).toEqual(ex); }); test('should call removePresenceMouseParticipant', () => { @@ -496,9 +492,40 @@ describe('MousePointers on HTML', () => { 'removePresenceMouseParticipant', ); - presenceMouseComponent['onParticipantsDidChange'](participants); - presenceMouseComponent['userBeingFollowedId'] = 'unit-test-participant2-ably-id'; - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant3-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant3-id', + }, + id: 'unit-test-participant3-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); + + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); + + presenceMouseComponent['userBeingFollowedId'] = 'unit-test-participant2-id'; + + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant3-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant3-id', + }, + id: 'unit-test-participant3-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); expect(removePresenceMouseParticipantSpy).toHaveBeenCalledTimes(1); }); @@ -509,7 +536,16 @@ describe('MousePointers on HTML', () => { 'updateParticipantsMouses', ); - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); expect(updateParticipantsMousesSpy).toHaveBeenCalled(); }); @@ -517,25 +553,30 @@ describe('MousePointers on HTML', () => { describe('goToMouse', () => { beforeEach(() => { - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); }); test('should call scrollIntoView', () => { presenceMouseComponent['start'](); presenceMouseComponent['createMouseElement']( - presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')!, + presenceMouseComponent['presences'].get('unit-test-participant2-id')!, ); - - presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')!.scrollIntoView = - jest.fn(); - + presenceMouseComponent['mouses'].get('unit-test-participant2-id')!.scrollIntoView = jest.fn(); presenceMouseComponent['start'](); - - presenceMouseComponent['goToMouse']('unit-test-participant2-ably-id'); + presenceMouseComponent['goToMouse']('unit-test-participant2-id'); expect( - presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')!.scrollIntoView, + presenceMouseComponent['mouses'].get('unit-test-participant2-id')!.scrollIntoView, ).toHaveBeenCalledWith({ block: 'center', inline: 'center', @@ -547,16 +588,15 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['start'](); presenceMouseComponent['createMouseElement']( - presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')!, + presenceMouseComponent['presences'].get('unit-test-participant2-id')!, ); - presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')!.scrollIntoView = - jest.fn(); + presenceMouseComponent['mouses'].get('unit-test-participant2-id')!.scrollIntoView = jest.fn(); presenceMouseComponent['goToMouse']('not-found'); expect( - presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')!.scrollIntoView, + presenceMouseComponent['mouses'].get('unit-test-participant2-id')!.scrollIntoView, ).not.toHaveBeenCalled(); }); @@ -564,19 +604,18 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['start'](); presenceMouseComponent['createMouseElement']( - presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')!, + presenceMouseComponent['presences'].get('unit-test-participant2-id')!, ); - presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')!.scrollIntoView = - jest.fn(); + presenceMouseComponent['mouses'].get('unit-test-participant2-id')!.scrollIntoView = jest.fn(); const { x, y } = presenceMouseComponent['mouses'] - .get('unit-test-participant2-ably-id')! + .get('unit-test-participant2-id')! .getBoundingClientRect(); const callback = jest.fn(); presenceMouseComponent['goToPresenceCallback'] = callback; - presenceMouseComponent['goToMouse']('unit-test-participant2-ably-id'); + presenceMouseComponent['goToMouse']('unit-test-participant2-id'); expect(callback).toHaveBeenCalledWith({ x, y }); }); @@ -584,39 +623,40 @@ describe('MousePointers on HTML', () => { describe('followMouse', () => { test('should set userBeingFollowedId', () => { - presenceMouseComponent['followMouse']('unit-test-participant2-ably-id'); - expect(presenceMouseComponent['userBeingFollowedId']).toEqual( - 'unit-test-participant2-ably-id', - ); + presenceMouseComponent['followMouse']('unit-test-participant2-id'); + expect(presenceMouseComponent['userBeingFollowedId']).toEqual('unit-test-participant2-id'); }); }); - describe('onParticipantLeftOnRealtime', () => { + describe('onPresenceLeftRoom', () => { test('should call removePresenceMouseParticipant', () => { const removePresenceMouseParticipantSpy = jest.spyOn( presenceMouseComponent as any, 'removePresenceMouseParticipant', ); - presenceMouseComponent['onParticipantLeftOnRealtime'](MOCK_MOUSE); + presenceMouseComponent['onPresenceLeftRoom']({ + connectionId: 'unit-test-participant2-id', + data: MOCK_MOUSE, + id: MOCK_MOUSE.id, + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); expect(removePresenceMouseParticipantSpy).toHaveBeenCalledWith(MOCK_MOUSE.id); }); }); describe('setParticipantPrivate', () => { - test('should call realtime.updatePresenceMouse', () => { + test('should call room.presence.update', () => { const updatePresenceMouseSpy = jest.spyOn( - presenceMouseComponent['realtime'], - 'updatePresenceMouse', + presenceMouseComponent['room']['presence'], + 'update', ); presenceMouseComponent['setParticipantPrivate'](true); - expect(updatePresenceMouseSpy).toHaveBeenCalledWith({ - ...MOCK_LOCAL_PARTICIPANT, - visible: false, - }); + expect(updatePresenceMouseSpy).toHaveBeenCalledWith({ visible: false }); }); test('should set isPrivate', () => { @@ -650,15 +690,15 @@ describe('MousePointers on HTML', () => { test('should create a mouse element and append it to the wrapper', () => { const mouse = presenceMouseComponent['createMouseElement']( - participants['unit-test-participant2-ably-id'], + participants['unit-test-participant2-id'], ); expect(mouse).toBeTruthy(); - expect(mouse.getAttribute('id')).toEqual('mouse-unit-test-participant2-ably-id'); + expect(mouse.getAttribute('id')).toEqual('mouse-unit-test-participant2-id'); expect(mouse.getAttribute('class')).toEqual('mouse-follower'); expect(mouse.querySelector('.pointer-mouse')).toBeTruthy(); expect(mouse.querySelector('.mouse-user-name')).toBeTruthy(); - expect(presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')).toEqual(mouse); + expect(presenceMouseComponent['mouses'].get('unit-test-participant2-id')).toEqual(mouse); }); test('should not create a mouse element if wrapper is not found', () => { @@ -666,37 +706,13 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['wrapper'] = undefined; const mouse = presenceMouseComponent['createMouseElement']( - participants['unit-test-participant3-ably-id'], + participants['unit-test-participant3-id'], ); expect(mouse).toBeFalsy(); }); }); - /** - * private getTextColorValue = (slotIndex: number): string => { - return [2, 4, 5, 7, 8, 16].includes(slotIndex) ? '#FFFFFF' : '#26242A'; - }; - - */ - describe('getTextColorValue', () => { - test('should return the correct colors', () => { - const indexArray: number[] = []; - for (let i = 0; i < 17; i++) { - indexArray.push(i); - } - - indexArray.forEach((index, i) => { - if (INDEX_IS_WHITE_TEXT.includes(i)) { - expect(presenceMouseComponent['getTextColorValue'](index)).toBe('#FFFFFF'); - return; - } - - expect(presenceMouseComponent['getTextColorValue'](index)).toBe('#26242A'); - }); - }); - }); - describe('updateSVGPosition', () => { beforeEach(() => { presenceMouseComponent['start'](); @@ -823,7 +839,16 @@ describe('MousePointers on HTML', () => { describe('updateParticipantsMouses', () => { beforeEach(() => { - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); }); test('should call removePresenceMouseParticipant if mouse is not visible', () => { @@ -832,12 +857,10 @@ describe('MousePointers on HTML', () => { 'removePresenceMouseParticipant', ); - presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')!.visible = false; + presenceMouseComponent['presences'].get('unit-test-participant2-id')!.visible = false; presenceMouseComponent['updateParticipantsMouses'](); - expect(removePresenceMouseParticipantSpy).toHaveBeenCalledWith( - 'unit-test-participant2-ably-id', - ); + expect(removePresenceMouseParticipantSpy).toHaveBeenCalledWith('unit-test-participant2-id'); }); test('should call renderPresenceMouses', () => { @@ -849,17 +872,17 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['updateParticipantsMouses'](); expect(renderPresenceMousesSpy).toHaveBeenCalledWith( - presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')!, + presenceMouseComponent['presences'].get('unit-test-participant2-id')!, ); }); test('should call goToMouse', () => { const goToMouseSpy = jest.spyOn(presenceMouseComponent as any, 'goToMouse'); - presenceMouseComponent['userBeingFollowedId'] = 'unit-test-participant2-ably-id'; + presenceMouseComponent['userBeingFollowedId'] = 'unit-test-participant2-id'; presenceMouseComponent['updateParticipantsMouses'](); - expect(goToMouseSpy).toHaveBeenCalledWith('unit-test-participant2-ably-id'); + expect(goToMouseSpy).toHaveBeenCalledWith('unit-test-participant2-id'); }); }); @@ -1049,23 +1072,32 @@ describe('MousePointers on HTML', () => { describe('removePresenceMouseParticipant', () => { beforeEach(() => { - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-id', + data: { + ...MOCK_MOUSE, + id: 'unit-test-participant2-id', + }, + id: 'unit-test-participant2-id', + name: MOCK_MOUSE.name as string, + timestamp: 1, + }); presenceMouseComponent['updateParticipantsMouses'](); }); test('should remove the mouse element and the participant from the presences and mouses', () => { const mouse = document.createElement('div'); - mouse.setAttribute('id', 'mouse-unit-test-participant2-ably-id'); + mouse.setAttribute('id', 'mouse-unit-test-participant2-id'); document.body.appendChild(mouse); - presenceMouseComponent['presences'].set('unit-test-participant2-ably-id', MOCK_MOUSE); - presenceMouseComponent['mouses'].set('unit-test-participant2-ably-id', mouse); + presenceMouseComponent['presences'].set('unit-test-participant2-id', MOCK_MOUSE); + presenceMouseComponent['mouses'].set('unit-test-participant2-id', mouse); - presenceMouseComponent['removePresenceMouseParticipant']('unit-test-participant2-ably-id'); + presenceMouseComponent['removePresenceMouseParticipant']('unit-test-participant2-id'); - expect(document.getElementById('mouse-unit-test-participant2-ably-id')).toBeFalsy(); - expect(presenceMouseComponent['presences'].get('unit-test-participant2-ably-id')).toBeFalsy(); - expect(presenceMouseComponent['mouses'].get('unit-test-participant2-ably-id')).toBeFalsy(); + expect(document.getElementById('mouse-unit-test-participant2-id')).toBeFalsy(); + expect(presenceMouseComponent['presences'].get('unit-test-participant2-id')).toBeFalsy(); + expect(presenceMouseComponent['mouses'].get('unit-test-participant2-id')).toBeFalsy(); }); test('should not remove the mouse element if it does not exist', () => { @@ -1116,25 +1148,17 @@ describe('MousePointers on HTML', () => { test('should create the mouse element if it does not exist', () => { const createMouseElementSpy = jest.spyOn(presenceMouseComponent as any, 'createMouseElement'); - presenceMouseComponent['renderPresenceMouses']( - participants['unit-test-participant2-ably-id'], - ); + presenceMouseComponent['renderPresenceMouses'](participants['unit-test-participant2-id']); - expect(createMouseElementSpy).toHaveBeenCalledWith( - participants['unit-test-participant2-ably-id'], - ); + expect(createMouseElementSpy).toHaveBeenCalledWith(participants['unit-test-participant2-id']); }); test('should not create the mouse element if it already exists', () => { const createMouseElementSpy = jest.spyOn(presenceMouseComponent as any, 'createMouseElement'); - presenceMouseComponent['renderPresenceMouses']( - participants['unit-test-participant2-ably-id'], - ); + presenceMouseComponent['renderPresenceMouses'](participants['unit-test-participant2-id']); - presenceMouseComponent['renderPresenceMouses']( - participants['unit-test-participant2-ably-id'], - ); + presenceMouseComponent['renderPresenceMouses'](participants['unit-test-participant2-id']); expect(createMouseElementSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 78bd36b1..fd615a9c 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -1,4 +1,5 @@ -import { isEqual } from 'lodash'; +import * as Socket from '@superviz/socket-client'; +import { isEqual, throttle } from 'lodash'; import { RealtimeEvent } from '../../../common/types/events.types'; import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; @@ -74,7 +75,6 @@ export class PointersHTML extends BaseComponent { */ protected start(): void { this.logger.log('presence-mouse component @ start'); - this.realtime.enterPresenceMouseChannel(this.localParticipant); this.renderWrapper(); this.addListeners(); @@ -95,7 +95,6 @@ export class PointersHTML extends BaseComponent { cancelAnimationFrame(this.animationFrame); this.logger.log('presence-mouse component @ destroy'); - this.realtime.leavePresenceMouseChannel(); this.removeListeners(); this.wrapper.remove(); @@ -121,8 +120,12 @@ export class PointersHTML extends BaseComponent { */ private subscribeToRealtimeEvents(): void { this.logger.log('presence-mouse component @ subscribe to realtime events'); - this.realtime.presenceMouseParticipantLeaveObserver.subscribe(this.onParticipantLeftOnRealtime); - this.realtime.presenceMouseObserver.subscribe(this.onParticipantsDidChange); + this.room.presence.on( + Socket.PresenceEvents.JOINED_ROOM, + this.onPresenceJoinedRoom, + ); + this.room.presence.on(Socket.PresenceEvents.LEAVE, this.onPresenceLeftRoom); + this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); } /** @@ -132,10 +135,9 @@ export class PointersHTML extends BaseComponent { */ private unsubscribeFromRealtimeEvents(): void { this.logger.log('presence-mouse component @ unsubscribe from realtime events'); - this.realtime.presenceMouseParticipantLeaveObserver.unsubscribe( - this.onParticipantLeftOnRealtime, - ); - this.realtime.presenceMouseObserver.unsubscribe(this.onParticipantsDidChange); + this.room.presence.off(Socket.PresenceEvents.JOINED_ROOM); + this.room.presence.off(Socket.PresenceEvents.LEAVE); + this.room.presence.off(Socket.PresenceEvents.UPDATE); } /** @@ -165,7 +167,7 @@ export class PointersHTML extends BaseComponent { * @description event to update my participant mouse position to others participants * @returns {void} */ - private onMyParticipantMouseMove = (event: MouseEvent): void => { + private onMyParticipantMouseMove = throttle((event: MouseEvent): void => { if (this.isPrivate) return; const container = event.currentTarget as HTMLDivElement; @@ -174,49 +176,20 @@ export class PointersHTML extends BaseComponent { const x = (event.x - left - this.transformation.translate.x) / this.transformation.scale; const y = (event.y - top - this.transformation.translate.y) / this.transformation.scale; - this.realtime.updatePresenceMouse({ + this.room.presence.update({ ...this.localParticipant, x, y, visible: true, }); - }; + }, 100); /** * @function onMyParticipantMouseLeave - * @param {MouseEvent} event - The MouseEvent object * @returns {void} */ private onMyParticipantMouseLeave = (): void => { - this.realtime.updatePresenceMouse({ visible: false, ...this.localParticipant }); - }; - - /** - * @function onParticipantsDidChange - * @description handler for participant list update event - * @param {Record} participants - participants - * @returns {void} - */ - private onParticipantsDidChange = (participants: Record): void => { - this.logger.log('presence-mouse component @ on participants did change', participants); - Object.values(participants).forEach((participant: ParticipantMouse) => { - if (participant.id === this.localParticipant.id) return; - const followingAnotherParticipant = - this.userBeingFollowedId && - participant.id !== this.userBeingFollowedId && - this.presences.has(participant.id); - - // When the user is following a participant, every other mouse pointer is removed - if (followingAnotherParticipant) { - this.removePresenceMouseParticipant(participant.id); - return; - } - - this.presences.set(participant.id, participant); - }); - - this.animate(); - this.updateParticipantsMouses(); + this.room.presence.update({ visible: false }); }; /** @@ -264,7 +237,55 @@ export class PointersHTML extends BaseComponent { */ private setParticipantPrivate = (isPrivate: boolean): void => { this.isPrivate = isPrivate; - this.realtime.updatePresenceMouse({ ...this.localParticipant, visible: !isPrivate }); + this.room.presence.update({ visible: !isPrivate }); + }; + + /** + * @function onPresenceJoinedRoom + * @description handler for presence joined room event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceJoinedRoom = (presence: Socket.PresenceEvent): void => { + if (presence.id !== this.localParticipant.id) return; + + this.room.presence.update(this.localParticipant); + }; + + /** + * @function onPresenceLeftRoom + * @description handler for presence left room event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceLeftRoom = (presence: Socket.PresenceEvent): void => { + this.removePresenceMouseParticipant(presence.id); + }; + + /** + * @function onPresenceUpdate + * @description handler for presence update event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceUpdate = (presence: Socket.PresenceEvent): void => { + if (presence.id === this.localParticipant.id) return; + const participant = presence.data; + + const followingAnotherParticipant = + this.userBeingFollowedId && + participant.id !== this.userBeingFollowedId && + this.presences.has(participant.id); + + // When the user is following a participant, every other mouse pointer is removed + if (followingAnotherParticipant) { + this.removePresenceMouseParticipant(participant.id); + return; + } + + this.presences.set(participant.id, participant); + this.animate(); + this.updateParticipantsMouses(); }; // ---------- HELPERS ---------- @@ -311,15 +332,6 @@ export class PointersHTML extends BaseComponent { return mouseFollower; } - /** - * @function getTextColorValue - * @param slotIndex - slot index - * @returns {string} - The color of the text in hex format - * */ - private getTextColorValue = (slotIndex: number): string => { - return INDEX_IS_WHITE_TEXT.includes(slotIndex) ? '#FFFFFF' : '#26242A'; - }; - /** * @function updateSVGPosition * @description - Updates the position of the wrapper of a element @@ -637,12 +649,12 @@ 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.slotIndex}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/pointers-v2/${participant.slot.index}.svg)`; } if (mouseUser) { - mouseUser.style.color = this.getTextColorValue(participant.slotIndex); - mouseUser.style.backgroundColor = participant.color; + mouseUser.style.color = participant.slot.textColor; + mouseUser.style.backgroundColor = participant.slot.color; mouseUser.innerHTML = participant.name; } diff --git a/src/components/presence-mouse/index.ts b/src/components/presence-mouse/index.ts index 7f18f3cd..b7f30c22 100644 --- a/src/components/presence-mouse/index.ts +++ b/src/components/presence-mouse/index.ts @@ -6,6 +6,11 @@ import { PresenceMouseProps } from './types'; export class MousePointers { constructor(containerId: string, options?: PresenceMouseProps) { const container = document.getElementById(containerId); + + if (!container) { + throw new Error(`[Superviz] Container with id ${containerId} not found`); + } + const tagName = container.tagName.toLowerCase(); if (tagName === 'canvas') { From 95bf8cc1d73eeffd42dadbd85bdf9721f3fdde4f Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 14 Mar 2024 19:04:17 -0300 Subject: [PATCH 33/83] feat: presence mouses canvas with new ioc --- .../presence-mouse/canvas/index.test.ts | 155 ++++++++++-------- src/components/presence-mouse/canvas/index.ts | 130 +++++++-------- .../presence-mouse/html/index.test.ts | 1 - src/components/presence-mouse/types.ts | 1 - src/core/launcher/index.ts | 2 + 5 files changed, 156 insertions(+), 133 deletions(-) diff --git a/src/components/presence-mouse/canvas/index.test.ts b/src/components/presence-mouse/canvas/index.test.ts index 0560ed55..b2a01cfa 100644 --- a/src/components/presence-mouse/canvas/index.test.ts +++ b/src/components/presence-mouse/canvas/index.test.ts @@ -13,7 +13,13 @@ const MOCK_MOUSE: ParticipantMouse = { ...MOCK_LOCAL_PARTICIPANT, x: 30, y: 30, - slotIndex: 0, + slot: { + index: 7, + color: '#304AFF', + textColor: '#fff', + colorName: 'bluedark', + timestamp: 1710448079918, + }, visible: true, }; @@ -81,48 +87,10 @@ describe('MousePointers on Canvas', () => { }); }); - describe('destroy', () => { - test('should unsubscribe from realtime events', () => { - presenceMouseComponent = createMousePointers(); - presenceMouseComponent['canvas'].removeEventListener = jest.fn(); - - presenceMouseComponent['destroy'](); - - expect(ABLY_REALTIME_MOCK.participantLeaveObserver.unsubscribe).toHaveBeenCalled(); - expect(ABLY_REALTIME_MOCK.participantsObserver.unsubscribe).toHaveBeenCalled(); - expect(presenceMouseComponent['divWrapper'].hasChildNodes()).toBeFalsy(); - expect(presenceMouseComponent['canvas'].removeEventListener).toBeCalled(); - }); - }); - - describe('subscribeToRealtimeEvents', () => { - test('should subscribe to realtime events', () => { - presenceMouseComponent['subscribeToRealtimeEvents'](); - - expect(ABLY_REALTIME_MOCK.participantLeaveObserver.subscribe).toHaveBeenCalledWith( - presenceMouseComponent['onParticipantLeftOnRealtime'], - ); - expect(ABLY_REALTIME_MOCK.participantsObserver.subscribe).toHaveBeenCalledWith( - presenceMouseComponent['onParticipantsDidChange'], - ); - }); - }); - - describe('unsubscribeFromRealtimeEvents', () => { - test('should subscribe to realtime events', () => { - presenceMouseComponent['unsubscribeFromRealtimeEvents'](); - - expect(ABLY_REALTIME_MOCK.participantLeaveObserver.unsubscribe).toHaveBeenCalledWith( - presenceMouseComponent['onParticipantLeftOnRealtime'], - ); - expect(ABLY_REALTIME_MOCK.participantsObserver.unsubscribe).toHaveBeenCalledWith( - presenceMouseComponent['onParticipantsDidChange'], - ); - }); - }); - describe('onMyParticipantMouseMove', () => { test('should update my participant mouse position', () => { + const spy = jest.spyOn(presenceMouseComponent['room']['presence'], 'update'); + const presenceContainerId = document.createElement('div'); presenceMouseComponent['containerId'] = 'container'; document.getElementById = jest.fn().mockReturnValue(presenceContainerId); @@ -130,7 +98,7 @@ describe('MousePointers on Canvas', () => { const event = { x: 10, y: 20 }; presenceMouseComponent['onMyParticipantMouseMove'](event as unknown as MouseEvent); - expect(ABLY_REALTIME_MOCK.updatePresenceMouse).toHaveBeenCalledWith({ + expect(spy).toHaveBeenCalledWith({ ...MOCK_LOCAL_PARTICIPANT, x: event.x, y: event.y, @@ -150,12 +118,17 @@ describe('MousePointers on Canvas', () => { test('should update presence mouse element for external participants', () => { presenceMouseComponent['updatePresenceMouseParticipant'] = jest.fn(); - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); // participant1 is local, so we don't want to update it const expected = new Map(); expected.set(participant2.id, participant2); - expected.set(participant3.id, participant3); expect(presenceMouseComponent['presences']).toEqual(expected); }); @@ -164,8 +137,22 @@ describe('MousePointers on Canvas', () => { presenceMouseComponent['goToMouse'] = jest .fn() .mockImplementation(presenceMouseComponent['goToMouse']); + + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); presenceMouseComponent['following'] = participant2.id; - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); expect(presenceMouseComponent['goToMouse']).toHaveBeenCalledWith(participant2.id); }); @@ -173,20 +160,33 @@ describe('MousePointers on Canvas', () => { test('should only update mouse being followed', () => { const firstExpected = new Map(); firstExpected.set(participant2.id, participant2); - firstExpected.set(participant3.id, participant3); - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); + expect(presenceMouseComponent['presences']).toEqual(firstExpected); const secondExpected = new Map(); secondExpected.set(participant2.id, participant2); + presenceMouseComponent['presences'] = new Map(); presenceMouseComponent['following'] = participant2.id; presenceMouseComponent['presences'].set(participant2.id, participant2); - presenceMouseComponent['presences'].set(participant3.id, participant3); - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); + expect(presenceMouseComponent['presences']).toEqual(secondExpected); }); }); @@ -198,11 +198,23 @@ describe('MousePointers on Canvas', () => { ...MOCK_LOCAL_PARTICIPANT, x: 1000, y: 1000, - slotIndex: 0, + slot: { + index: 7, + color: '#304AFF', + textColor: '#fff', + colorName: 'bluedark', + timestamp: 1710448079918, + }, visible: true, }; - presenceMouseComponent['onParticipantLeftOnRealtime'](MOCK_MOUSE); + presenceMouseComponent['onPresenceLeftRoom']({ + connectionId: MOCK_MOUSE.id, + id: MOCK_MOUSE.id, + name: MOCK_MOUSE.name as string, + timestamp: 1, + data: MOCK_MOUSE, + }); expect(spy).toHaveBeenCalledWith(MOCK_MOUSE.id); }); @@ -214,19 +226,26 @@ describe('MousePointers on Canvas', () => { ...MOCK_LOCAL_PARTICIPANT, x: 1000, y: 1000, - slotIndex: 0, + slot: { + index: 7, + color: '#304AFF', + textColor: '#fff', + colorName: 'bluedark', + timestamp: 1710448079918, + }, visible: true, }; const participant2 = MOCK_MOUSE; participant2.id = 'unit-test-participant2-ably-id'; - const participants: Record = { - participant: MOCK_MOUSE, - participant2, - }; - - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); presenceMouseComponent['goToMouseCallback'] = jest.fn(); presenceMouseComponent['goToMouse']('not-found-user-id'); @@ -235,7 +254,13 @@ describe('MousePointers on Canvas', () => { }); test('should call callback if user id is found', () => { - presenceMouseComponent['onParticipantsDidChange'](participants); + presenceMouseComponent['onPresenceUpdate']({ + connectionId: 'unit-test-participant2-ably-id', + data: participant2, + id: 'unit-test-participant2-ably-id', + name: participant2.name as string, + timestamp: 1, + }); presenceMouseComponent['goToMouseCallback'] = jest.fn(); presenceMouseComponent['goToMouse'](participant2.id); @@ -278,13 +303,11 @@ describe('MousePointers on Canvas', () => { jest.restoreAllMocks(); }); test('should publish own mouse visibility as false to realtime', () => { + const spy = jest.spyOn(presenceMouseComponent['room']['presence'], 'update'); const event = new MouseEvent('mouseout'); presenceMouseComponent['onMyParticipantMouseOut'](event); - expect(ABLY_REALTIME_MOCK.updatePresenceMouse).toHaveBeenCalledWith({ - ...MOCK_LOCAL_PARTICIPANT, - visible: false, - } as ParticipantMouse); + expect(spy).toHaveBeenCalledWith({ visible: false }); }); }); @@ -329,13 +352,11 @@ describe('MousePointers on Canvas', () => { }); test('should update presenceMouse in realtime', () => { + const spy = jest.spyOn(presenceMouseComponent['room']['presence'], 'update'); const isPrivate = true; presenceMouseComponent['setParticipantPrivate'](isPrivate); - expect(presenceMouseComponent['realtime'].updatePresenceMouse).toBeCalledWith({ - ...presenceMouseComponent['localParticipant'], - visible: !isPrivate, - }); + expect(spy).toBeCalledWith({ visible: !isPrivate }); }); }); diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 4bf95455..651d1577 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -1,8 +1,10 @@ +import * as Socket from '@superviz/socket-client'; +import { throttle } from 'lodash'; + import { RealtimeEvent } from '../../../common/types/events.types'; import { Participant } from '../../../common/types/participant.types'; import { StoreType } from '../../../common/types/stores.types'; import { Logger } from '../../../common/utils'; -import { useGlobalStore } from '../../../services/stores'; import { BaseComponent } from '../../base'; import { ComponentNames } from '../../types'; import { ParticipantMouse, PresenceMouseProps, Transform } from '../types'; @@ -42,10 +44,6 @@ export class PointersCanvas extends BaseComponent { localParticipant.subscribe(); } - private get textColorValues(): number[] { - return [2, 4, 5, 7, 8, 16]; - } - /** * @function start * @description start presence-mouse component @@ -60,7 +58,6 @@ export class PointersCanvas extends BaseComponent { this.eventBus.subscribe(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, this.followMouse); this.eventBus.subscribe(RealtimeEvent.REALTIME_PRIVATE_MODE, this.setParticipantPrivate); this.subscribeToRealtimeEvents(); - this.realtime.enterPresenceMouseChannel(this.localParticipant); } /** @@ -73,7 +70,6 @@ export class PointersCanvas extends BaseComponent { this.eventBus.unsubscribe(RealtimeEvent.REALTIME_GO_TO_PARTICIPANT, this.goToMouse); this.eventBus.unsubscribe(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, this.followMouse); this.eventBus.unsubscribe(RealtimeEvent.REALTIME_PRIVATE_MODE, this.setParticipantPrivate); - this.realtime.leavePresenceMouseChannel(); this.unsubscribeFromRealtimeEvents(); cancelAnimationFrame(this.animateFrame); @@ -89,8 +85,12 @@ export class PointersCanvas extends BaseComponent { */ private subscribeToRealtimeEvents = (): void => { this.logger.log('presence-mouse component @ subscribe to realtime events'); - this.realtime.presenceMouseParticipantLeaveObserver.subscribe(this.onParticipantLeftOnRealtime); - this.realtime.presenceMouseObserver.subscribe(this.onParticipantsDidChange); + this.room.presence.on( + Socket.PresenceEvents.JOINED_ROOM, + this.onPresenceJoinedRoom, + ); + this.room.presence.on(Socket.PresenceEvents.LEAVE, this.onPresenceLeftRoom); + this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); }; /** @@ -100,10 +100,9 @@ export class PointersCanvas extends BaseComponent { */ private unsubscribeFromRealtimeEvents = (): void => { this.logger.log('presence-mouse component @ unsubscribe from realtime events'); - this.realtime.presenceMouseParticipantLeaveObserver.unsubscribe( - this.onParticipantLeftOnRealtime, - ); - this.realtime.presenceMouseObserver.unsubscribe(this.onParticipantsDidChange); + this.room.presence.off(Socket.PresenceEvents.JOINED_ROOM); + this.room.presence.off(Socket.PresenceEvents.LEAVE); + this.room.presence.off(Socket.PresenceEvents.UPDATE); }; /** @@ -113,7 +112,51 @@ export class PointersCanvas extends BaseComponent { */ private setParticipantPrivate = (isPrivate: boolean): void => { this.isPrivate = isPrivate; - this.realtime.updatePresenceMouse({ ...this.localParticipant, visible: !isPrivate }); + this.room.presence.update({ visible: !isPrivate }); + }; + + /** + * @function onPresenceJoinedRoom + * @description handler for presence joined room event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceJoinedRoom = (presence: Socket.PresenceEvent): void => { + if (presence.id !== this.localParticipant.id) return; + + this.room.presence.update(this.localParticipant); + }; + + /** + * @function onPresenceLeftRoom + * @description handler for presence left room event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceLeftRoom = (presence: Socket.PresenceEvent): void => { + this.removePresenceMouseParticipant(presence.id); + }; + + /** + * @function onPresenceUpdate + * @description handler for presence update event + * @param {PresenceEvent} presence + * @returns {void} + */ + private onPresenceUpdate = (presence: Socket.PresenceEvent): void => { + if (this.following && this.presences.get(this.following)) { + this.goToMouse(this.following); + } + + if (presence.id === this.localParticipant.id) return; + const participant = presence.data; + + if (this.following && participant.id !== this.following && this.presences.has(participant.id)) { + this.removePresenceMouseParticipant(participant.id); + return; + } + + this.presences.set(participant.id, participant); }; /** @@ -159,7 +202,7 @@ export class PointersCanvas extends BaseComponent { * @description event to update my participant mouse position to others participants * @returns {void} */ - private onMyParticipantMouseMove = (event: MouseEvent): void => { + private onMyParticipantMouseMove = throttle((event: MouseEvent): void => { const context = this.canvas.getContext('2d'); const rect = this.canvas.getBoundingClientRect(); const x = event.x - rect.left; @@ -174,59 +217,19 @@ export class PointersCanvas extends BaseComponent { y: (transformedPoint.y - this.transformation.translate.y) / this.transformation.scale, }; - this.realtime.updatePresenceMouse({ + this.room.presence.update({ ...this.localParticipant, ...coordinates, visible: !this.isPrivate, }); - }; - - /** - * @function onParticipantsDidChange - * @description handler for participant list update event - * @param {Record} participants - participants - * @returns {void} - */ - private onParticipantsDidChange = (participants: Record): void => { - this.logger.log('presence-mouse component @ on participants did change', participants); - - Object.values(participants).forEach((participant: ParticipantMouse) => { - if (participant.id === this.localParticipant.id) return; - if ( - this.following && - participant.id !== this.following && - this.presences.has(participant.id) - ) { - this.removePresenceMouseParticipant(participant.id); - return; - } - - this.presences.set(participant.id, participant); - }); - - if (this.following) { - const mouse = this.presences.get(this.following); - if (mouse) { - this.goToMouse(this.following); - } - } - }; + }, 100); private onMyParticipantMouseOut = (event: MouseEvent): void => { const { x, y, width, height } = this.canvas.getBoundingClientRect(); - if (event.x > 0 && event.y > 0 && event.x < x + width && event.y < y + height) return; - this.realtime.updatePresenceMouse({ visible: false, ...this.localParticipant }); - }; + if (event.x > 0 && event.y > 0 && event.x < x + width && event.y < y + height) return; - /** - * @function onParticipantLeftOnRealtime - * @description handler for participant left event - * @param {AblyParticipant} participant - * @returns {void} - */ - private onParticipantLeftOnRealtime = (participant: ParticipantMouse): void => { - this.removePresenceMouseParticipant(participant.id); + this.room.presence.update({ visible: false }); }; /** @@ -290,14 +293,12 @@ 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.slotIndex}.svg)`; + pointerUser.style.backgroundImage = `url(https://production.cdn.superviz.com/static/pointers-v2/${mouse.slot.index}.svg)`; } if (mouseUser) { - mouseUser.style.color = this.textColorValues.includes(mouse.slotIndex) - ? '#FFFFFF' - : '#26242A'; - mouseUser.style.backgroundColor = mouse.color; + mouseUser.style.color = mouse.slot.textColor; + mouseUser.style.backgroundColor = mouse.slot.color; mouseUser.innerHTML = mouse.name; } @@ -329,6 +330,7 @@ export class PointersCanvas extends BaseComponent { * */ private removePresenceMouseParticipant(participantId: string): void { const userMouseIdExist = document.getElementById(`mouse-${participantId}`); + if (userMouseIdExist) { userMouseIdExist.remove(); this.presences.delete(participantId); diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index 1235244e..ce691b3b 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -35,7 +35,6 @@ describe('MousePointers on HTML', () => { ...MOCK_LOCAL_PARTICIPANT, x: 30, y: 30, - slotIndex: 7, slot: { index: 7, color: '#304AFF', diff --git a/src/components/presence-mouse/types.ts b/src/components/presence-mouse/types.ts index 7b74c38a..ec91bb9f 100644 --- a/src/components/presence-mouse/types.ts +++ b/src/components/presence-mouse/types.ts @@ -1,7 +1,6 @@ import { Participant } from '../../common/types/participant.types'; export interface ParticipantMouse extends Participant { - slotIndex: number; x: number; y: number; visible: boolean; diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index dc4c10cb..e7d963c7 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -401,6 +401,8 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {void} */ private onParticipantUpdatedIOC = (presence: Socket.PresenceEvent): void => { + console.log('onParticipantUpdatedIOC', presence); + if ( presence.id === this.participant.value.id && !isEqual(this.participant.value, presence.data) From 0f307416e77290e76c8680bf2adc3b69edf3a9cc Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 14 Mar 2024 19:06:56 -0300 Subject: [PATCH 34/83] refactor: remove presences mouses functions from ably --- src/services/realtime/ably/index.ts | 66 ----------------------------- src/services/realtime/base/index.ts | 7 --- 2 files changed, 73 deletions(-) diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 420898d6..47fa6303 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -41,7 +41,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably private clientRoomStateChannel: Ably.Types.RealtimeChannelCallbacks = null; private broadcastChannel: Ably.Types.RealtimeChannelCallbacks = null; private presenceWIOChannel: Ably.Types.RealtimeChannelCallbacks = null; - private presenceMouseChannel: Ably.Types.RealtimeChannelCallbacks = null; private presence3DChannel: Ably.Types.RealtimeChannelCallbacks = null; private clientRoomState: Record = {}; private clientSyncPropertiesQueue: Record = {}; @@ -939,71 +938,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.commentsChannel.publish('update', annotations); }; - /** Presence Mouse */ - - public enterPresenceMouseChannel = (participant: Participant): void => { - if (!this.presenceMouseChannel) { - this.presenceMouseChannel = this.client.channels.get( - `${this.roomId.toLowerCase()}-presence-mouse`, - ); - this.presenceMouseChannel.attach(); - - this.presenceMouseChannel.presence.subscribe('enter', this.onPresenceMouseChannelEnter); - this.presenceMouseChannel.presence.subscribe('leave', this.onPresenceMouseChannelLeave); - this.presenceMouseChannel.presence.subscribe('update', this.publishPresenceMouseUpdate); - } - - this.presenceMouseChannel.presence.enter(participant); - }; - - public leavePresenceMouseChannel = (): void => { - if (!this.presenceMouseChannel) return; - - this.presenceMouseChannel.presence.leave(); - this.presenceMouseChannel = null; - }; - - public updatePresenceMouse = throttle((data: Partial): void => { - const participant = Object.assign({}, this.participantsMouse[data.id] || {}, data); - - this.presenceMouseChannel.presence.update(participant); - }, SYNC_MOUSE_INTERVAL); - - private onPresenceMouseChannelEnter = (presence: Ably.Types.PresenceMessage): void => { - const slot = this.getParticipantSlot(presence.clientId); - - this.participantsMouse[presence.clientId] = { - ...presence.data, - slotIndex: slot, - color: this.getSlotColor(slot).color, - }; - - this.presenceMouseParticipantJoinedObserver.publish(this.participantsMouse[presence.clientId]); - }; - - private onPresenceMouseChannelLeave = (presence: Ably.Types.PresenceMessage): void => { - this.presenceMouseParticipantLeaveObserver.publish(this.participantsMouse[presence.clientId]); - delete this.participantsMouse[presence.clientId]; - }; - - /** - * @function publishPresenceMouseUpdate - * @param {AblyParticipant} participant - * @description publish a participant's changes to observer - * @returns {void} - */ - private publishPresenceMouseUpdate = (presence: Ably.Types.PresenceMessage): void => { - const slot = this.getParticipantSlot(presence.clientId); - - this.participantsMouse[presence.clientId] = { - ...presence.data, - slotIndex: slot, - color: this.getSlotColor(slot).color, - }; - - this.presenceMouseObserver.publish(this.participantsMouse); - }; - /** Who Is Online */ public enterWIOChannel = (participant: Participant): void => { diff --git a/src/services/realtime/base/index.ts b/src/services/realtime/base/index.ts index 3ef54f05..98b9aa53 100644 --- a/src/services/realtime/base/index.ts +++ b/src/services/realtime/base/index.ts @@ -23,9 +23,6 @@ export class RealtimeService implements DefaultRealtimeService { public privateModeWIOObserver: Observer; public followWIOObserver: Observer; public gatherWIOObserver: Observer; - public presenceMouseParticipantLeaveObserver: Observer; - public presenceMouseParticipantJoinedObserver: Observer; - public presenceSlotsInfosObserver: Observer; public presence3dObserver: Observer; public presence3dLeaveObserver: Observer; public presence3dJoinedObserver: Observer; @@ -62,10 +59,6 @@ export class RealtimeService implements DefaultRealtimeService { this.followWIOObserver = new Observer({ logger: this.logger }); this.gatherWIOObserver = new Observer({ logger: this.logger }); - this.presenceMouseParticipantLeaveObserver = new Observer({ logger: this.logger }); - this.presenceMouseParticipantJoinedObserver = new Observer({ logger: this.logger }); - this.presenceSlotsInfosObserver = new Observer({ logger: this.logger }); - // presence 3d this.presence3dObserver = new Observer({ logger: this.logger }); From ba2062af295b05c3095a0872f2ffc9d87a00be4d Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 15 Mar 2024 08:39:49 -0300 Subject: [PATCH 35/83] chore: remove log --- src/core/launcher/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index e7d963c7..dc4c10cb 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -401,8 +401,6 @@ export class Launcher extends Observable implements DefaultLauncher { * @returns {void} */ private onParticipantUpdatedIOC = (presence: Socket.PresenceEvent): void => { - console.log('onParticipantUpdatedIOC', presence); - if ( presence.id === this.participant.value.id && !isEqual(this.participant.value, presence.data) From 21fee0e4bf033f9d7cb348ed5f755fe7b38bf6ac Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 15 Mar 2024 16:13:40 -0300 Subject: [PATCH 36/83] feat: realtime and mouse throttles --- src/common/styles/global.css | 1 - .../presence-mouse/html/index.test.ts | 2 -- src/components/presence-mouse/html/index.ts | 26 +++++++------------ src/components/realtime/index.test.ts | 1 + src/components/realtime/index.ts | 5 ++-- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/common/styles/global.css b/src/common/styles/global.css index a7ae530b..3ade86f3 100644 --- a/src/common/styles/global.css +++ b/src/common/styles/global.css @@ -75,5 +75,4 @@ html, body { position: absolute; display: block; z-index: 2; - transition: all 300ms ease-in-out; } \ No newline at end of file diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index ce691b3b..ae4ec21f 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -216,7 +216,6 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['addListeners'](); - expect(addEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function)); expect(addEventListenerSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function)); }); }); @@ -230,7 +229,6 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['removeListeners'](); - expect(removeEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function)); expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function)); }); }); diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index fd615a9c..13bcbc6b 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -1,8 +1,8 @@ import * as Socket from '@superviz/socket-client'; -import { isEqual, throttle } from 'lodash'; +import { isEqual } from 'lodash'; +import { Subscription, fromEvent, throttleTime } from 'rxjs'; import { RealtimeEvent } from '../../../common/types/events.types'; -import { INDEX_IS_WHITE_TEXT } from '../../../common/types/meeting-colors.types'; import { Participant } from '../../../common/types/participant.types'; import { StoreType } from '../../../common/types/stores.types'; import { Logger } from '../../../common/utils'; @@ -36,6 +36,7 @@ export class PointersHTML extends BaseComponent { private isPrivate: boolean; private containerTagname: string; private transformation: Transform = { translate: { x: 0, y: 0 }, scale: 1 }; + private pointerMoveObserver: Subscription; // callbacks private goToPresenceCallback: PresenceMouseProps['onGoToPresence']; @@ -143,11 +144,12 @@ export class PointersHTML extends BaseComponent { /** * @function addListeners * @description adds the mousemove and mouseout listeners to the wrapper with the specified id - * @param {string} id the id of the wrapper * @returns {void} */ private addListeners(): void { - this.container.addEventListener('pointermove', this.onMyParticipantMouseMove); + this.pointerMoveObserver = fromEvent(this.container, 'pointermove') + .pipe(throttleTime(10)) + .subscribe(this.onMyParticipantMouseMove); this.container.addEventListener('mouseleave', this.onMyParticipantMouseLeave); } @@ -157,7 +159,7 @@ export class PointersHTML extends BaseComponent { * @returns {void} */ private removeListeners(): void { - this.container.removeEventListener('pointermove', this.onMyParticipantMouseMove); + this.pointerMoveObserver?.unsubscribe(); this.container.removeEventListener('mouseleave', this.onMyParticipantMouseLeave); } @@ -167,7 +169,7 @@ export class PointersHTML extends BaseComponent { * @description event to update my participant mouse position to others participants * @returns {void} */ - private onMyParticipantMouseMove = throttle((event: MouseEvent): void => { + private onMyParticipantMouseMove = (event: MouseEvent): void => { if (this.isPrivate) return; const container = event.currentTarget as HTMLDivElement; @@ -182,7 +184,7 @@ export class PointersHTML extends BaseComponent { y, visible: true, }); - }, 100); + }; /** * @function onMyParticipantMouseLeave @@ -220,16 +222,6 @@ export class PointersHTML extends BaseComponent { this.userBeingFollowedId = id; }; - /** - * @function onParticipantLeftOnRealtime - * @description handler for participant left event - * @param {AblyParticipant} participant - * @returns {void} - */ - private onParticipantLeftOnRealtime = (participant: ParticipantMouse): void => { - this.removePresenceMouseParticipant(participant.id); - }; - /** * @function setParticipantPrivate * @description perform animation in presence mouse diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index a0485908..90262fd3 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -12,6 +12,7 @@ import { RealtimeComponentState } from './types'; import { Realtime } from '.'; +jest.mock('lodash/throttle', () => jest.fn((fn) => fn)); jest.useFakeTimers(); describe('realtime component', () => { diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index b7cd8d9f..e1c39d56 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -1,4 +1,5 @@ import * as Socket from '@superviz/socket-client'; +import throttle from 'lodash/throttle'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { StoreType } from '../../common/types/stores.types'; @@ -49,7 +50,7 @@ export class Realtime extends BaseComponent { * @param data - event data * @returns {void} */ - public publish = (event: string, data?: unknown): void => { + public publish = throttle((event: string, data?: unknown): void => { if (Object.values(ComponentLifeCycleEvent).includes(event as ComponentLifeCycleEvent)) { this.publishEventToClient(event, data); return; @@ -63,7 +64,7 @@ export class Realtime extends BaseComponent { } this.room.emit('message', { name: event, payload: data }); - }; + }, 100); /** * @function subscribe From 184b8a3516dc87585c831c044b03401cbae916e9 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 18 Mar 2024 10:02:24 -0300 Subject: [PATCH 37/83] feat: export Transform type used in presence mouse --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index a8e7b191..d2b0f59e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,8 +47,11 @@ export { PinAdapter, PinCoordinates, AnnotationPositionInfo, + Offset, } from './components/comments/types'; +export { Transform } from './components/presence-mouse/types'; + if (window) { window.SuperVizRoom = { init, From cf25a0d87b2a109ae01b5f40cb08f7f63d089a60 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 18 Mar 2024 12:57:47 -0300 Subject: [PATCH 38/83] chore(deps): bump socket client version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c78559c4..064ae836 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.2.1", + "@superviz/socket-client": "1.2.2", "ably": "^1.2.45", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", diff --git a/yarn.lock b/yarn.lock index 373e0dd2..b9ca051e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2481,10 +2481,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.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.2.1.tgz#9a4ceb9bc2c07361210756e66eb866991e8ceaed" - integrity sha512-8H9APdKPxyPncJmwTshu+VxzBxHCiLP/5TwAkWLnr/f5cPPsRxrm/7Ws51y1yiObz9bNkniwphER6qV4dUdCOw== +"@superviz/socket-client@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.2.2.tgz#6f228e84588051ca092e827c172af6c8f18182f5" + integrity sha512-JDvKBtyamCe00Tf0KM5Uaac73LVafcLKlByPxXBc5wTmcEo/cuLeaQ4sbpQzUUwJaS2ptnbvZMmYkhL3SnW1AQ== dependencies: "@reactivex/rxjs" "^6.6.7" debug "^4.3.4" From af16d0c33be8dc512dc0d2a8d09b6bb49f4be536 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 18 Mar 2024 14:33:02 -0300 Subject: [PATCH 39/83] fix: change way of export Transform type --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d2b0f59e..d8e9319c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { HTMLPin, WhoIsOnline, } from './components'; +import { Transform } from './components/presence-mouse/types'; import { RealtimeComponentEvent, RealtimeComponentState } from './components/realtime/types'; import init from './core'; import './web-components'; @@ -50,8 +51,6 @@ export { Offset, } from './components/comments/types'; -export { Transform } from './components/presence-mouse/types'; - if (window) { window.SuperVizRoom = { init, @@ -100,6 +99,7 @@ export { CommentEvent, ComponentLifeCycleEvent, WhoIsOnlineEvent, + Transform, }; export default init; From 301b156d93d0a3a4682d778aec0e68718dc8bc64 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 19 Mar 2024 10:30:53 -0300 Subject: [PATCH 40/83] fix: build types --- package.json | 2 +- tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 064ae836..51297573 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "postbuild": "./node_modules/typescript/bin/tsc --emitDeclarationOnly --declaration", "watch": "concurrently -n code,types \"yarn watch:code\" \"yarn watch:types\"", "watch:code": "node ./.esbuild/watch.js", - "watch:types": "./node_modules/typescript/bin/tsc --watch --outDir dist", + "watch:types": "./node_modules/typescript/bin/tsc --watch --out ./dist/index.d.ts", "test:unit": "jest", "test:unit:watch": "jest --watch", "test:unit:coverage": "jest --coverage", diff --git a/tsconfig.json b/tsconfig.json index 33b81dee..91e90279 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ES2020", - "outDir": "./lib", + "outFile": "./lib/index.d.ts", "lib": ["ES2020", "DOM"], "preserveWatchOutput": true, "emitDeclarationOnly": true, @@ -14,7 +14,7 @@ "allowJs": true, }, "include": [ - "./src" + "./src/**/*.ts" ], "exclude": [ "./src/**/*.test.ts", From 0558eef6cb47b86ff4636097e11605c0cfed32dc Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 19 Mar 2024 14:13:55 -0300 Subject: [PATCH 41/83] fix: build types and add suport to ESM entrypoint --- .esbuild/build.js | 18 +++++++++++++++++- .esbuild/config.js | 3 ++- .esbuild/watch.js | 19 +++++++++++++++++-- .remote-config.d.ts | 4 ++++ .version.d.ts | 1 + package.json | 5 +++-- tsconfig.json | 3 ++- 7 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 .remote-config.d.ts create mode 100644 .version.d.ts diff --git a/.esbuild/build.js b/.esbuild/build.js index 9f7f2fd6..5c6e8dbb 100644 --- a/.esbuild/build.js +++ b/.esbuild/build.js @@ -6,7 +6,23 @@ const config = Object.assign({}, baseConfig, { }); require('esbuild') - .build(config) + .build({ + ...config, + platform: 'node', // for CJS + outfile: 'lib/index.js', + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); + +require('esbuild') + .build({ + ...config, + platform: 'neutral', // for ESM + format: 'esm', + outfile: 'lib/index.esm.js', + }) .catch((error) => { console.error(error); process.exit(1); diff --git a/.esbuild/config.js b/.esbuild/config.js index dc6b16ea..85c16c5b 100644 --- a/.esbuild/config.js +++ b/.esbuild/config.js @@ -1,5 +1,6 @@ require('dotenv').config(); const { style } = require('./plugins/style-loader'); +const { dependencies } = require('../package.json'); const entries = Object.entries(process.env).filter((key) => key[0].startsWith('SDK_')); const env = Object.fromEntries(entries); @@ -18,7 +19,7 @@ module.exports = { bundle: true, target: 'es6', format: 'esm', - outdir: 'lib', + external: Object.keys(dependencies), define: { 'process.env': JSON.stringify(env), }, diff --git a/.esbuild/watch.js b/.esbuild/watch.js index b193c0c9..45551a36 100644 --- a/.esbuild/watch.js +++ b/.esbuild/watch.js @@ -2,11 +2,26 @@ const baseConfig = require('./config'); const config = Object.assign({}, baseConfig, { watch: true, - outdir: 'dist', }); require('esbuild') - .build(config) + .build({ + ...config, + platform: 'node', // for CJS + outfile: 'dist/index.js', + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); + +require('esbuild') + .build({ + ...config, + platform: 'neutral', // for ESM + format: 'esm', + outfile: 'dist/index.esm.js', + }) .catch((error) => { console.error(error); process.exit(1); diff --git a/.remote-config.d.ts b/.remote-config.d.ts new file mode 100644 index 00000000..6f7c2f33 --- /dev/null +++ b/.remote-config.d.ts @@ -0,0 +1,4 @@ +export namespace remoteConfig { + let apiUrl: string; + let conferenceLayerUrl: string; +} diff --git a/.version.d.ts b/.version.d.ts new file mode 100644 index 00000000..4b2c8905 --- /dev/null +++ b/.version.d.ts @@ -0,0 +1 @@ +export const version: "lab"; diff --git a/package.json b/package.json index 51297573..2bea9c7a 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.0-development", "description": "SuperViz SDK", "main": "./lib/index.js", + "module": "./lib/index.esm.js", "types": "./lib/index.d.ts", "files": [ "lib" @@ -10,10 +11,10 @@ "scripts": { "prepare": "husky install", "build": "node ./.esbuild/build.js", - "postbuild": "./node_modules/typescript/bin/tsc --emitDeclarationOnly --declaration", + "postbuild": "./node_modules/typescript/bin/tsc", "watch": "concurrently -n code,types \"yarn watch:code\" \"yarn watch:types\"", "watch:code": "node ./.esbuild/watch.js", - "watch:types": "./node_modules/typescript/bin/tsc --watch --out ./dist/index.d.ts", + "watch:types": "./node_modules/typescript/bin/tsc --watch --outDir ./dist", "test:unit": "jest", "test:unit:watch": "jest --watch", "test:unit:coverage": "jest --coverage", diff --git a/tsconfig.json b/tsconfig.json index 91e90279..446d4ac3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,9 @@ { "compilerOptions": { + "rootDir": "./src", "target": "ES2020", "module": "ES2020", - "outFile": "./lib/index.d.ts", + "outDir": "./lib", "lib": ["ES2020", "DOM"], "preserveWatchOutput": true, "emitDeclarationOnly": true, From f3779bdd27b675dd0ac41d5fff0c97f9086e5b3b Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 19 Mar 2024 14:27:10 -0300 Subject: [PATCH 42/83] fix: test runner --- tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 446d4ac3..c2f8412e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "rootDir": "./src", + "rootDirs": ["./src", "."], "target": "ES2020", "module": "ES2020", "outDir": "./lib", @@ -15,10 +15,10 @@ "allowJs": true, }, "include": [ - "./src/**/*.ts" + "./src" ], "exclude": [ "./src/**/*.test.ts", "node_modules" ] -} +} \ No newline at end of file From 62e022257cb57c1e7d6a723d05332b1a3a41021d Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 20 Mar 2024 09:51:41 -0300 Subject: [PATCH 43/83] fix: update slot on ably when it's changed on new io --- src/core/launcher/index.ts | 9 ++++++++- src/services/slot/index.test.ts | 8 ++++---- src/services/slot/index.ts | 32 +++++++++++++------------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index dc4c10cb..7c605eab 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -355,7 +355,7 @@ export class Launcher extends Observable implements DefaultLauncher { private onParticipantJoinedIOC = (presence: Socket.PresenceEvent): void => { if (presence.id === this.participant.value.id) { // Assign a slot to the participant - SlotService.register(this.LauncherRealtimeRoom, this.realtime, this.participant.value); + SlotService.register(this.LauncherRealtimeRoom, this.participant.value); this.LauncherRealtimeRoom.presence.update(this.participant.value); } @@ -420,6 +420,13 @@ export class Launcher extends Observable implements DefaultLauncher { this.publish(ParticipantEvent.LOCAL_UPDATED, presence.data); this.logger.log('Publishing ParticipantEvent.UPDATED', presence.data); + + if ( + presence.data?.slot?.index !== undefined && + presence.data?.slot?.index !== this.realtime.participant.data.slotIndex + ) { + this.realtime.updateMyProperties({ slotIndex: presence.data.slot.index }); + } } this.participants.value.set(presence.id, presence.data); diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index f0a21b4a..1c8ef002 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -16,7 +16,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); + const instance = new SlotService(room, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); @@ -47,7 +47,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); + const instance = new SlotService(room, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); @@ -80,7 +80,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); + const instance = new SlotService(room, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeUndefined(); @@ -114,7 +114,7 @@ describe('slot service', () => { id: '123', } as any; - const instance = new SlotService(room, { updateMyProperties: jest.fn() } as any, participant); + const instance = new SlotService(room, participant); await instance['assignSlot'](); expect(instance['slotIndex']).toBeDefined(); diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index 0b50794e..fe3a2a38 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -12,26 +12,20 @@ export class SlotService { private room: Socket.Room; private participant: Participant; private slotIndex: number; - private realtime: AblyRealtimeService; private static instance: SlotService; // @NOTE - reciving old realtime service instance until we migrate to new IO - constructor(room: Socket.Room, realtime: AblyRealtimeService, participant: Participant) { + constructor(room: Socket.Room, participant: Participant) { this.room = room; this.participant = participant; - this.realtime = realtime; this.assignSlot(); this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); } - public static register( - room: Socket.Room, - realtime: AblyRealtimeService, - participant: Participant, - ) { + public static register(room: Socket.Room, participant: Participant) { if (!SlotService.instance) { - SlotService.instance = new SlotService(room, realtime, participant); + SlotService.instance = new SlotService(room, participant); } return SlotService.instance; @@ -89,16 +83,16 @@ export class SlotService { // @NOTE - this is a temporary fix for the issue where the slot is not being updated in the presence // @TODO - remove this once we remove the colors from the old io - if (!this.realtime.isJoinedRoom) { - await new Promise((resolve) => { - setTimeout(resolve, 1500); - }); - } - - this.realtime.updateMyProperties({ - slotIndex: slot, - slot: slotData, - }); + // if (!this.realtime.isJoinedRoom) { + // await new Promise((resolve) => { + // setTimeout(resolve, 1500); + // }); + // } + + // this.realtime.updateMyProperties({ + // slotIndex: slot, + // slot: slotData, + // }); }) .catch((error) => { this.room.presence.update({ From 7e7aed3c270708fd9b080acac04c3cc82e52d88c Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 20 Mar 2024 09:51:58 -0300 Subject: [PATCH 44/83] fix: update participant type when starts the video --- src/common/styles/global.css | 2 +- src/components/video/index.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/common/styles/global.css b/src/common/styles/global.css index 3ade86f3..cd58b4b9 100644 --- a/src/common/styles/global.css +++ b/src/common/styles/global.css @@ -17,7 +17,7 @@ html, body { margin: 0; padding: 0; overflow: hidden; - z-index: 5; + z-index: 1000; } #sv-video-wrapper iframe.sv-video-frame--right { diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 723a4c6d..023aa82c 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -146,10 +146,6 @@ export class VideoConference extends BaseComponent { protected start(): void { this.logger.log('video conference @ start'); - if (this.params.userType !== ParticipantType.GUEST) { - this.localParticipant.type = this.params.userType as ParticipantType; - } - this.suscribeToRealtimeEvents(); this.startVideo(); } @@ -415,6 +411,16 @@ export class VideoConference extends BaseComponent { if (state !== VideoFrameState.INITIALIZED) return; + if (this.params.userType !== ParticipantType.GUEST) { + this.localParticipant = Object.assign(this.localParticipant, { + type: this.params.userType, + }); + + this.realtime.updateMyProperties({ + ...this.localParticipant, + }); + } + this.videoManager.start({ group: this.group, participant: this.localParticipant, From 7aeb96fb8212a499f38d68ffa9c13d76a0705d6c Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 20 Mar 2024 09:52:54 -0300 Subject: [PATCH 45/83] refactor: remove commented code --- src/services/slot/index.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index fe3a2a38..0aa6ffb3 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -80,19 +80,6 @@ export class SlotService { this.room.presence.update({ slot: slotData, }); - - // @NOTE - this is a temporary fix for the issue where the slot is not being updated in the presence - // @TODO - remove this once we remove the colors from the old io - // if (!this.realtime.isJoinedRoom) { - // await new Promise((resolve) => { - // setTimeout(resolve, 1500); - // }); - // } - - // this.realtime.updateMyProperties({ - // slotIndex: slot, - // slot: slotData, - // }); }) .catch((error) => { this.room.presence.update({ From 83d0938b10fe8c5c6613c0ba947cc1a4bcc59c21 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Wed, 20 Mar 2024 11:04:10 -0300 Subject: [PATCH 46/83] fix: adjust new IO throttle --- src/common/styles/global.css | 1 + src/components/presence-mouse/html/index.test.ts | 4 ++-- src/components/presence-mouse/html/index.ts | 6 +++--- src/components/realtime/index.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/common/styles/global.css b/src/common/styles/global.css index cd58b4b9..60e5d0e6 100644 --- a/src/common/styles/global.css +++ b/src/common/styles/global.css @@ -75,4 +75,5 @@ html, body { position: absolute; display: block; z-index: 2; + transition: all 150ms linear, opacity 100s ease-in; } \ No newline at end of file diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index 3c1f2fd2..7a09f9ad 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -216,7 +216,7 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['addListeners'](); - expect(addEventListenerSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('pointerleave', expect.any(Function)); }); }); @@ -229,7 +229,7 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['removeListeners'](); - expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('pointerleave', expect.any(Function)); }); }); diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 0aa2c2be..5bddf157 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -148,9 +148,9 @@ export class PointersHTML extends BaseComponent { */ private addListeners(): void { this.pointerMoveObserver = fromEvent(this.container, 'pointermove') - .pipe(throttleTime(10)) + .pipe(throttleTime(200)) .subscribe(this.onMyParticipantMouseMove); - this.container.addEventListener('mouseleave', this.onMyParticipantMouseLeave); + this.container.addEventListener('pointerleave', this.onMyParticipantMouseLeave); } /** @@ -160,7 +160,7 @@ export class PointersHTML extends BaseComponent { */ private removeListeners(): void { this.pointerMoveObserver?.unsubscribe(); - this.container.removeEventListener('mouseleave', this.onMyParticipantMouseLeave); + this.container.removeEventListener('pointerleave', this.onMyParticipantMouseLeave); } // ---------- CALLBACKS ---------- diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index e1c39d56..a0e32ab5 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -64,7 +64,7 @@ export class Realtime extends BaseComponent { } this.room.emit('message', { name: event, payload: data }); - }, 100); + }, 200); /** * @function subscribe From 306cb108ee4befe4852972fa8ed8a98fea863237 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 21 Mar 2024 09:14:53 -0300 Subject: [PATCH 47/83] fix: fetchHistory this --- src/components/realtime/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index a0e32ab5..b09e5195 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -104,9 +104,9 @@ export class Realtime extends BaseComponent { * @description get realtime client data history * @returns {RealtimeMessage | Record} */ - public async fetchHistory( + public fetchHistory = async ( eventName?: string, - ): Promise | null> { + ): Promise | null> => { const history: RealtimeMessage[] | Record = await new Promise( (resolve, reject) => { const next = (data: Socket.RoomHistory) => { @@ -149,7 +149,7 @@ export class Realtime extends BaseComponent { ); return history; - } + }; /** * @function changeState From d5e930aba4a2b18247c2fbb22116760f9cf61bde Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 21 Mar 2024 12:14:22 -0300 Subject: [PATCH 48/83] fix: publish realtime state changes --- src/components/realtime/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index b09e5195..71ad0fae 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -168,12 +168,15 @@ export class Realtime extends BaseComponent { this.room.presence.on(Socket.PresenceEvents.JOINED_ROOM, (event) => { if (event.id !== this.localParticipant.id) return; - this.logger.log('joined room'); this.changeState(RealtimeComponentState.STARTED); this.callbacksToSubscribeWhenJoined.forEach(({ event, callback }) => { this.subscribe(event, callback); }); + + this.logger.log('joined room'); + // publishing again to make sure all clients know that we are connected + this.changeState(RealtimeComponentState.STARTED); }); this.room.on('message', (event) => { From 13ddf428a3f70a97649504a445dc8d5c8cf30835 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 21 Mar 2024 14:52:46 -0300 Subject: [PATCH 49/83] feat: up realtime and presences to 30 messages per second --- src/components/presence-mouse/canvas/index.ts | 2 +- src/components/presence-mouse/html/index.ts | 2 +- src/components/realtime/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 890c4ca6..b1107010 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -222,7 +222,7 @@ export class PointersCanvas extends BaseComponent { ...coordinates, visible: !this.isPrivate, }); - }, 100); + }, 30); private onMyParticipantMouseOut = (event: MouseEvent): void => { const { x, y, width, height } = this.canvas.getBoundingClientRect(); diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 5bddf157..aa24d8c7 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -148,7 +148,7 @@ export class PointersHTML extends BaseComponent { */ private addListeners(): void { this.pointerMoveObserver = fromEvent(this.container, 'pointermove') - .pipe(throttleTime(200)) + .pipe(throttleTime(30)) .subscribe(this.onMyParticipantMouseMove); this.container.addEventListener('pointerleave', this.onMyParticipantMouseLeave); } diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index 71ad0fae..03e881da 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -64,7 +64,7 @@ export class Realtime extends BaseComponent { } this.room.emit('message', { name: event, payload: data }); - }, 200); + }, 30); /** * @function subscribe From c428b8aae7486dda414ea5d68e17b2b02d516cfa Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 26 Mar 2024 11:57:14 -0300 Subject: [PATCH 50/83] fix: wait to update participant data to emit participant joined --- src/core/launcher/index.test.ts | 2 +- src/core/launcher/index.ts | 37 ++++++------ src/services/slot/index.test.ts | 71 +++++++---------------- src/services/slot/index.ts | 100 ++++++++++++++------------------ 4 files changed, 84 insertions(+), 126 deletions(-) diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index 1ae2ba28..7535cfd0 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -165,7 +165,7 @@ describe('Launcher', () => { const callback = jest.fn(); LauncherInstance.subscribe(ParticipantEvent.JOINED, callback); - LauncherInstance['onParticipantJoinedIOC']({ + LauncherInstance['onParticipantUpdatedIOC']({ connectionId: 'connection1', data: MOCK_LOCAL_PARTICIPANT, id: MOCK_LOCAL_PARTICIPANT.id, diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 7c605eab..174cd155 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -352,28 +352,25 @@ export class Launcher extends Observable implements DefaultLauncher { * @param presence - participant presence * @returns {void} */ - private onParticipantJoinedIOC = (presence: Socket.PresenceEvent): void => { - if (presence.id === this.participant.value.id) { - // Assign a slot to the participant - SlotService.register(this.LauncherRealtimeRoom, this.participant.value); - this.LauncherRealtimeRoom.presence.update(this.participant.value); - } + private onParticipantJoinedIOC = async ( + presence: Socket.PresenceEvent, + ): Promise => { + if (presence.id !== this.participant.value.id) return; - // When the participant joins, it is without any data, it's updated later - this.participants.value.set(presence.id, { - id: presence.id, - name: presence.name, - ...presence.data, - }); + // Assign a slot to the participant + const slot = new SlotService(this.LauncherRealtimeRoom, this.participant.value); + const slotData = await slot.assignSlot(); - if (presence.id === this.participant.value.id) { - this.logger.log('launcher service @ onParticipantJoined - local participant joined'); - this.publish(ParticipantEvent.LOCAL_JOINED, this.participant.value); - } + this.participant.value = { + ...this.participant.value, + slot: slotData, + }; + + this.LauncherRealtimeRoom.presence.update(this.participant.value); - this.logger.log('launcher service @ onParticipantJoined - participant joined', presence.data); + this.logger.log('launcher service @ onParticipantJoined - local participant joined'); - this.publish(ParticipantEvent.JOINED, this.participants.value.get(presence.id)); + this.publish(ParticipantEvent.LOCAL_JOINED, this.participant.value); }; /** @@ -429,6 +426,10 @@ export class Launcher extends Observable implements DefaultLauncher { } } + if (!this.participants.value.has(presence.id)) { + this.publish(ParticipantEvent.JOINED, presence.data); + } + this.participants.value.set(presence.id, presence.data); const participantList = Array.from(this.participants.value.values()); diff --git a/src/services/slot/index.test.ts b/src/services/slot/index.test.ts index 1c8ef002..f4305061 100644 --- a/src/services/slot/index.test.ts +++ b/src/services/slot/index.test.ts @@ -1,38 +1,11 @@ import { SlotService } from '.'; describe('slot service', () => { - it('should assign a slot to the participant', async () => { - const room = { - presence: { - on: jest.fn(), - get: jest.fn((callback) => { - callback([]); - }), - update: jest.fn(), - }, - } as any; - - const participant = { - id: '123', - } as any; - - const instance = new SlotService(room, participant); - await instance['assignSlot'](); - - expect(instance['slotIndex']).toBeDefined(); - expect(instance['participant'].slot).toBeDefined(); - expect(room.presence.update).toHaveBeenCalledWith({ - slot: expect.objectContaining({ - index: expect.any(Number), - color: expect.any(String), - textColor: expect.any(String), - colorName: expect.any(String), - timestamp: expect.any(Number), - }), - }); + afterEach(() => { + jest.resetAllMocks(); }); - it('should assign a slot to the participant', async () => { + test('should assign a slot to the participant', async () => { const room = { presence: { on: jest.fn(), @@ -48,22 +21,20 @@ describe('slot service', () => { } as any; const instance = new SlotService(room, participant); - await instance['assignSlot'](); + const result = await instance.assignSlot(); expect(instance['slotIndex']).toBeDefined(); expect(instance['participant'].slot).toBeDefined(); - expect(room.presence.update).toHaveBeenCalledWith({ - slot: expect.objectContaining({ - index: expect.any(Number), - color: expect.any(String), - textColor: expect.any(String), - colorName: expect.any(String), - timestamp: expect.any(Number), - }), + expect(result).toEqual({ + index: expect.any(Number), + color: expect.any(String), + textColor: expect.any(String), + colorName: expect.any(String), + timestamp: expect.any(Number), }); }); - it('if there are no more slots available, it should throw an error', async () => { + test('if there are no more slots available, it should throw an error', async () => { console.error = jest.fn(); const room = { @@ -81,13 +52,13 @@ describe('slot service', () => { } as any; const instance = new SlotService(room, participant); - await instance['assignSlot'](); + const result = await instance.assignSlot(); expect(instance['slotIndex']).toBeUndefined(); expect(instance['participant'].slot).toBeUndefined(); }); - it('if the slot is already in use, it should assign a new slot', async () => { + test('if the slot is already in use, it should assign a new slot', async () => { const room = { presence: { on: jest.fn(), @@ -115,18 +86,16 @@ describe('slot service', () => { } as any; const instance = new SlotService(room, participant); - await instance['assignSlot'](); + const result = await instance.assignSlot(); expect(instance['slotIndex']).toBeDefined(); expect(instance['participant'].slot).toBeDefined(); - expect(room.presence.update).toHaveBeenCalledWith({ - slot: expect.objectContaining({ - index: expect.any(Number), - color: expect.any(String), - textColor: expect.any(String), - colorName: expect.any(String), - timestamp: expect.any(Number), - }), + expect(result).toEqual({ + index: expect.any(Number), + 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 0aa6ffb3..df4fc876 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -5,7 +5,7 @@ import { MeetingColors, MeetingColorsHex, } from '../../common/types/meeting-colors.types'; -import { Participant } from '../../common/types/participant.types'; +import { Participant, Slot } from '../../common/types/participant.types'; import { AblyRealtimeService } from '../realtime'; export class SlotService { @@ -19,77 +19,64 @@ export class SlotService { this.room = room; this.participant = participant; - this.assignSlot(); this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onPresenceUpdate); } - public static register(room: Socket.Room, participant: Participant) { - if (!SlotService.instance) { - SlotService.instance = new SlotService(room, participant); - } - - return SlotService.instance; - } - /** * @function assignSlot * @description Assigns a slot to the participant * @returns void */ - private async assignSlot() { - const slot = Math.floor(Math.random() * 16); + public async assignSlot(): Promise { + try { + const slot = Math.floor(Math.random() * 16); - new Promise((resolve, reject) => { - this.room.presence.get((presences) => { - if (!presences || !presences.length) resolve(false); + const isUsing = await new Promise((resolve, reject) => { + this.room.presence.get((presences) => { + if (!presences || !presences.length) resolve(false); - if (presences.length >= 16) { - reject(new Error('[SuperViz] - No more slots available')); - return; - } + if (presences.length >= 16) { + reject(new Error('[SuperViz] - No more slots available')); + return; + } - presences.forEach((presence: Socket.PresenceEvent) => { - if (presence.id === this.participant.id) return; + presences.forEach((presence: Socket.PresenceEvent) => { + if (presence.id === this.participant.id) return; - if (presence.data?.slot?.index === slot) resolve(true); - }); + if (presence.data?.slot?.index === slot) resolve(true); + }); - resolve(false); - }); - }) - .then(async (isUsing) => { - if (isUsing) { - this.assignSlot(); - return; - } - - const slotData = { - index: slot, - color: MeetingColorsHex[slot], - textColor: INDEX_IS_WHITE_TEXT.includes(slot) ? '#fff' : '#000', - colorName: MeetingColors[slot], - timestamp: Date.now(), - }; - - this.slotIndex = slot; - this.participant = { - ...this.participant, - slot: slotData, - }; - - this.room.presence.update({ - slot: slotData, - }); - }) - .catch((error) => { - this.room.presence.update({ - slot: null, + resolve(false); }); - console.error(error); }); + + if (isUsing) { + const slotData = await this.assignSlot(); + return slotData; + } + + const slotData = { + index: slot, + color: MeetingColorsHex[slot], + textColor: INDEX_IS_WHITE_TEXT.includes(slot) ? '#fff' : '#000', + colorName: MeetingColors[slot], + timestamp: Date.now(), + }; + + this.slotIndex = slot; + this.participant = { + ...this.participant, + slot: slotData, + }; + + return slotData; + } catch (error) { + console.error(error); + return null; + } } - private onPresenceUpdate = (event: Socket.PresenceEvent) => { + private onPresenceUpdate = async (event: Socket.PresenceEvent) => { if (!event.data.slot || !this.participant?.slot) return; if (event.id === this.participant.id) { @@ -102,7 +89,8 @@ export class SlotService { // if someone else has the same slot as me, and they were assigned first, I should reassign if (event.data.slot?.index === this.slotIndex && slotOccupied) { - this.assignSlot(); + const slotData = await this.assignSlot(); + this.room.presence.update({ slot: slotData }); } }; } From 9215ee8ee872514df8517055d1662664f4589b9e Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 2 Apr 2024 07:34:10 -0300 Subject: [PATCH 51/83] feat: create flag for disabling presence controls --- src/common/types/stores.types.ts | 7 +++- src/common/utils/use-store.ts | 10 +++-- src/components/who-is-online/index.ts | 16 ++------ src/components/who-is-online/types.ts | 3 ++ src/services/stores/who-is-online/index.ts | 16 ++++---- .../who-is-online/components/dropdown.ts | 22 +++++++--- .../who-is-online/who-is-online.ts | 41 +++++++++++++++---- 7 files changed, 77 insertions(+), 38 deletions(-) diff --git a/src/common/types/stores.types.ts b/src/common/types/stores.types.ts index 270bfbe4..43af99a2 100644 --- a/src/common/types/stores.types.ts +++ b/src/common/types/stores.types.ts @@ -1,5 +1,6 @@ import { useGlobalStore } from '../../services/stores'; import { PublicSubject } from '../../services/stores/common/types'; +import { useWhoIsOnlineStore } from '../../services/stores/who-is-online/index'; export enum StoreType { GLOBAL = 'global-store', @@ -18,5 +19,9 @@ type StoreApi any> = { // When creating new Stores, expand the ternary with the new Store. For example: // ...T extends StoreType.GLOBAL ? StoreApi : T extends StoreType.WHO_IS_ONLINE ? StoreApi : never; // Yes, it will be a little bit verbose, but it's not like we'll be creating more and more Stores just for. Rarely will someone need to come here -export type Store = T extends StoreType.GLOBAL ? StoreApi : never; +export type Store = T extends StoreType.GLOBAL + ? StoreApi + : T extends StoreType.WHO_IS_ONLINE + ? StoreApi + : never; export type StoresTypes = typeof StoreType; diff --git a/src/common/utils/use-store.ts b/src/common/utils/use-store.ts index 40d78ed6..90e20c56 100644 --- a/src/common/utils/use-store.ts +++ b/src/common/utils/use-store.ts @@ -1,9 +1,11 @@ import { PublicSubject } from '../../services/stores/common/types'; import { useGlobalStore } from '../../services/stores/global'; +import { useWhoIsOnlineStore } from '../../services/stores/who-is-online'; import { Store, StoreType } from '../types/stores.types'; const stores = { [StoreType.GLOBAL]: useGlobalStore, + [StoreType.WHO_IS_ONLINE]: useWhoIsOnlineStore, }; /** @@ -19,16 +21,16 @@ function subscribeTo( callback?: (value: T) => void, ): void { subject.subscribe(this, () => { - this[name] = subject.value; - if (callback) { callback(subject.value); + } else { + this[name] = subject.value; } + + if (this.requestUpdate) this.requestUpdate(); }); this.unsubscribeFrom.push(subject.unsubscribe); - - if (this.requestUpdate) this.requestUpdate(); } /** diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index 2e036557..cb3732ae 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -32,6 +32,9 @@ export class WhoIsOnline extends BaseComponent { this.position = options.position ?? Position.TOP_RIGHT; this.setStyles(options.styles); + + const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + disablePresenceControls.publish(options.flags?.disablePresenceControls); } /** @@ -136,7 +139,7 @@ export class WhoIsOnline extends BaseComponent { */ private onParticipantListUpdate = (data: Record): void => { const updatedParticipants = Object.values(data).filter(({ data }) => { - return data.activeComponents?.includes('whoIsOnline'); + return data.activeComponents?.includes('whoIsOnline') || data.id === this.localParticipantId; }); const participants = updatedParticipants @@ -148,7 +151,6 @@ export class WhoIsOnline extends BaseComponent { const { color } = this.realtime.getSlotColor(slotIndex); const isLocal = this.localParticipantId === id; const joinedPresence = activeComponents.some((component) => component.includes('presence')); - this.setLocalData(isLocal, !joinedPresence, joinedPresence); return { name, id, slotIndex, color, isLocal, joinedPresence, avatar }; }); @@ -164,16 +166,6 @@ export class WhoIsOnline extends BaseComponent { this.element.updateParticipants(this.participants); }; - private setLocalData = (local: boolean, disable: boolean, joinedPresence: boolean) => { - if (!local) return; - - this.element.disableDropdown = disable; - this.element.localParticipantData = { - ...this.element.localParticipantData, - joinedPresence, - }; - }; - /** * @function setStyles * @param {string} styles - The user custom styles to be added to the who is online diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 419b59ca..17c41de7 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -19,4 +19,7 @@ export type WhoIsOnlinePosition = Position | `${Position}` | string | ''; export interface WhoIsOnlineOptions { position?: WhoIsOnlinePosition; styles?: string; + flags?: { + disablePresenceControls?: boolean; + } } diff --git a/src/services/stores/who-is-online/index.ts b/src/services/stores/who-is-online/index.ts index 4b93a0f2..db2bd93a 100644 --- a/src/services/stores/who-is-online/index.ts +++ b/src/services/stores/who-is-online/index.ts @@ -1,4 +1,3 @@ -import { Participant } from '../../../common/types/participant.types'; import { Singleton } from '../common/types'; import { CreateSingleton } from '../common/utils'; import subject from '../subject'; @@ -6,29 +5,32 @@ import subject from '../subject'; const instance: Singleton = CreateSingleton(); export class WhoIsOnlineStore { - public participantHasJoinedPresence = subject(null); + public disablePresenceControls = subject(false); constructor() { if (instance.value) { - throw new Error('CommentsStore is a singleton. There can only be one instance of it.'); + throw new Error('WhoIsOnlineStore is a singleton. There can only be one instance of it.'); } instance.value = this; } public destroy() { - this.participantHasJoinedPresence.destroy(); + this.disablePresenceControls.destroy(); instance.value = null; } } const store = new WhoIsOnlineStore(); -const participantHasJoinedPresence = store.participantHasJoinedPresence.expose(); -const destroy = store.destroy.bind(store); +const destroy = store.destroy.bind(store) as () => void; + +const disablePresenceControls = store.disablePresenceControls.expose(); export function useWhoIsOnlineStore() { return { - participantHasJoinedPresence, + disablePresenceControls, destroy, }; } + +export type WhoIsOnlineStoreReturnType = ReturnType; diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 2f1789e3..3a97c18e 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -4,6 +4,7 @@ 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 { Participant } from '../../../components/who-is-online/types'; import { WebComponentsBase } from '../../base'; import importStyle from '../../base/utils/importStyle'; @@ -31,6 +32,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { declare dropdownList: HTMLElement; private animationFrame: number; + private disablePresenceControls: boolean; static properties = { open: { type: Boolean }, @@ -48,9 +50,11 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { constructor() { super(); - // should match presence-mouse textColorValues this.selected = ''; this.showParticipantTooltip = true; + + const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + disablePresenceControls.subscribe(); } protected firstUpdated( @@ -104,8 +108,10 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { }); }; - private selectParticipant = (participantId: string) => { + private selectParticipant = (participantId: string, disableDropdown: boolean) => { return () => { + if (disableDropdown) return; + this.selected = participantId; }; }; @@ -148,8 +154,8 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { (participant) => participant.id, (participant, index) => { const { id, slotIndex, joinedPresence, isLocal, color, name } = participant; - - const disableDropdown = !joinedPresence || isLocal || this.disableDropdown; + const disableDropdown = + !joinedPresence || isLocal || this.disableDropdown || this.disablePresenceControls; const contentClasses = { 'who-is-online__extra-participant': true, @@ -179,7 +185,11 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { name, }; - if (this.localParticipantJoinedPresence && joinedPresence) { + if ( + this.localParticipantJoinedPresence && + joinedPresence && + !this.disablePresenceControls + ) { tooltipData.action = 'Click to Follow'; } @@ -204,7 +214,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { >
+ @click=${this.selectParticipant(id, disableDropdown)} slot="dropdown">
${this.getAvatar(participant)} diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index 4e2e6590..cae9bdd8 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -23,18 +23,20 @@ export class WhoIsOnline extends WebComponentsBaseElement { declare position: string; declare participants: Participant[]; declare open: boolean; - declare disableDropdown: boolean; declare following: Following | undefined; - declare localParticipantData: LocalParticipantData; declare isPrivate: boolean; declare everyoneFollowsMe: boolean; declare showTooltip: boolean; + private localParticipantData: LocalParticipantData; + private disablePresenceControls: boolean; + + private disableDropdown: boolean; + static properties = { position: { type: String }, participants: { type: Object }, open: { type: Boolean }, - disableDropdown: { type: Boolean }, following: { type: Object }, localParticipantColor: { type: String }, isPrivate: { type: Boolean }, @@ -49,12 +51,24 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.open = false; const { localParticipant } = this.useStore(StoreType.GLOBAL); + localParticipant.subscribe((value: Participant) => { + const joinedPresence = value.activeComponents?.some((component) => + component.toLowerCase().includes('presence'), + ); + this.localParticipantData = { id: value.id, - joinedPresence: false, + joinedPresence: value.activeComponents?.some((component) => + component.toLowerCase().includes('presence'), + ), }; + + this.disableDropdown = !joinedPresence; }); + + const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + disablePresenceControls.subscribe(); } protected firstUpdated( @@ -226,6 +240,8 @@ export class WhoIsOnline extends WebComponentsBaseElement { } private getOptions(participant: Participant, isBeingFollowed: boolean, isLocal: boolean) { + if (this.disablePresenceControls) return []; + const { id, slotIndex, name, color } = participant; const baseOption = { id, name, color, slotIndex }; const { isPrivate } = this; @@ -247,13 +263,15 @@ export class WhoIsOnline extends WebComponentsBaseElement { } private getIcons(isLocal: boolean, isBeingFollowed: boolean) { + if (this.disablePresenceControls) return []; + return isLocal ? ['gather', this.everyoneFollowsMe ? 'send-off' : 'send', 'eye_inative'] : ['place', isBeingFollowed ? 'send-off' : 'send']; } private putLocalParticipationFirst() { - if (this.participants[0].isLocal) return; + if (!this.participants[0] || this.participants[0].isLocal) return; const localParticipant = this.participants?.find(({ isLocal }) => isLocal); if (!localParticipant) return; @@ -300,12 +318,12 @@ export class WhoIsOnline extends WebComponentsBaseElement { (participant) => participant.id, (participant, index) => { const { joinedPresence, isLocal, id, name, color } = participant; - const participantIsFollowed = this.following?.id === id; const options = this.getOptions(participant, participantIsFollowed, isLocal); const icons = this.getIcons(isLocal, participantIsFollowed); const position = this.dropdownPosition(index); - const disableDropdown = !joinedPresence || this.disableDropdown; + const disableDropdown = + !joinedPresence || this.disableDropdown || this.disablePresenceControls; const classList = { 'who-is-online__participant': true, @@ -321,7 +339,14 @@ export class WhoIsOnline extends WebComponentsBaseElement { name, }; - if (this.localParticipantData?.joinedPresence && joinedPresence && !isLocal) { + const showAction = !this.disablePresenceControls; + + if ( + showAction && + this.localParticipantData?.joinedPresence && + joinedPresence && + !isLocal + ) { tooltipData.action = 'Click to Follow'; } From c0acd9fc2f01c1556c8f29615d58a6bd242f8635 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 2 Apr 2024 07:34:58 -0300 Subject: [PATCH 52/83] fix: update mouse positions after setting transform --- src/components/presence-mouse/html/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index aa24d8c7..3e3cfe8e 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -205,7 +205,10 @@ export class PointersHTML extends BaseComponent { if (!pointer) return; if (this.goToPresenceCallback) { - const { x, y } = this.mouses.get(id).getBoundingClientRect(); + const mouse = this.mouses.get(id); + const x = Number(mouse.style.left.replace('px', '')); + const y = Number(mouse.style.top.replace('px', '')); + this.goToPresenceCallback({ x, y }); return; } @@ -491,6 +494,7 @@ export class PointersHTML extends BaseComponent { */ public transform(transformation: Transform) { this.transformation = transformation; + this.updateParticipantsMouses(true); } /** @@ -510,7 +514,7 @@ export class PointersHTML extends BaseComponent { this.animationFrame = requestAnimationFrame(this.animate); }; - private updateParticipantsMouses = (): void => { + private updateParticipantsMouses = (haltFollow?: boolean): void => { this.presences.forEach((mouse) => { if (mouse.id === this.localParticipant.id) return; @@ -522,6 +526,8 @@ export class PointersHTML extends BaseComponent { this.renderPresenceMouses(mouse); }); + if (haltFollow) return; + const isFollowingSomeone = this.presences.has(this.userBeingFollowedId); if (isFollowingSomeone) { this.goToMouse(this.userBeingFollowedId); @@ -656,7 +662,8 @@ export class PointersHTML extends BaseComponent { scale, } = this.transformation; - mouseFollower.style.transform = `translate(${baseX + x * scale}px, ${baseY + y * scale}px)`; + mouseFollower.style.left = `${baseX + x * scale}px`; + mouseFollower.style.top = `${baseY + y * scale}px`; }; /** From b4f9fd263648ae36b2b3dabd9c4e5b81574b7492 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 2 Apr 2024 08:03:43 -0300 Subject: [PATCH 53/83] fix: correct broken tests --- .../who-is-online/components/dropdown.test.ts | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 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 1d8bdd26..c3823ef0 100644 --- a/src/web-components/who-is-online/components/dropdown.test.ts +++ b/src/web-components/who-is-online/components/dropdown.test.ts @@ -177,7 +177,7 @@ describe('who-is-online-dropdown', () => { const letter = element()?.shadowRoot?.querySelector('.who-is-online__participant__avatar'); - const backgroundColor = MeetingColorsHex[mockParticipants[0].slotIndex]; + const backgroundColor = MeetingColorsHex[mockParticipants[0].slotIndex as number]; expect(letter?.getAttribute('style')).toBe( `background-color: ${backgroundColor}; color: #26242A`, ); @@ -218,7 +218,22 @@ describe('who-is-online-dropdown', () => { }); test('should change selected participant when click on it', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ + position: 'bottom', + participants: [ + { + avatar: { + imageUrl: '', + model3DUrl: '', + }, + color: MeetingColorsHex[0], + id: '1', + name: 'John Zero', + slotIndex: 0, + joinedPresence: true, + }, + ], + }); await sleep(); @@ -233,6 +248,22 @@ describe('who-is-online-dropdown', () => { expect(element()?.['selected']).toBe(mockParticipants[0].id); }); + test('should not change selected participant when click on it if not in presence', async () => { + createEl({ position: 'bottom', participants: mockParticipants }); + + await sleep(); + + const participant = element()?.shadowRoot?.querySelector( + '.who-is-online__extra-participant', + ) as HTMLElement; + + participant.click(); + + await sleep(); + + expect(element()?.['selected']).not.toBe(mockParticipants[0].id); + }); + describe('repositionDropdown', () => { test('should call reposition methods if is open', () => { const el = createEl({ position: 'bottom', participants: mockParticipants }); From 0f1d73147ad2e85e19323b9ce4c4f47fbdb10cab Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 2 Apr 2024 09:33:43 -0300 Subject: [PATCH 54/83] fix: make participant being hovered with higher z-index --- src/web-components/who-is-online/css/dropdown.style.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/web-components/who-is-online/css/dropdown.style.ts b/src/web-components/who-is-online/css/dropdown.style.ts index 8a398d55..9ae006aa 100644 --- a/src/web-components/who-is-online/css/dropdown.style.ts +++ b/src/web-components/who-is-online/css/dropdown.style.ts @@ -84,6 +84,11 @@ export const dropdownStyle = css` overflow: auto; } + .who-is-online__extras-dropdown superviz-dropdown:hover { + z-index: 999; + position: relative; + } + .who-is-online__extras__arrow-icon { display: flex; align-items: center; From 2c0fcaebe6914dbd86d425e73639eabec51f5317 Mon Sep 17 00:00:00 2001 From: vitorvargasdev Date: Wed, 3 Apr 2024 09:49:31 -0300 Subject: [PATCH 55/83] fix: ensure focus and send button in editing mode --- .../comments/components/comment-input.ts | 12 ++++++++++++ .../comments/components/comment-item.ts | 1 + 2 files changed, 13 insertions(+) diff --git a/src/web-components/comments/components/comment-input.ts b/src/web-components/comments/components/comment-input.ts index 38284d60..e1aea337 100644 --- a/src/web-components/comments/components/comment-input.ts +++ b/src/web-components/comments/components/comment-input.ts @@ -10,6 +10,8 @@ import { commentInputStyle } from '../css'; import { AutoCompleteHandler } from '../utils/autocomplete-handler'; import mentionHandler from '../utils/mention-handler'; +import { CommentMode } from './types'; + const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, commentInputStyle]; @@ -25,6 +27,7 @@ export class CommentsCommentInput extends WebComponentsBaseElement { declare mentions: CommentMention[]; declare participantsList: ParticipantByGroupApi[]; declare hideInput: boolean; + declare mode: CommentMode; private pinCoordinates: AnnotationPositionInfo | null = null; @@ -36,6 +39,7 @@ export class CommentsCommentInput extends WebComponentsBaseElement { this.text = ''; this.mentionList = []; this.mentions = []; + this.mode = CommentMode.READONLY; } static styles = styles; @@ -50,6 +54,7 @@ export class CommentsCommentInput extends WebComponentsBaseElement { mentionList: { type: Object }, participantsList: { type: Object }, hideInput: { type: Boolean }, + mode: { type: String }, }; private addAtSymbolInCaretPosition = () => { @@ -139,6 +144,13 @@ export class CommentsCommentInput extends WebComponentsBaseElement { } updated(changedProperties: Map) { + if (changedProperties.has('mode') && this.mode === CommentMode.EDITABLE) { + this.focusInput() + this.updateHeight(); + this.sendBtn.disabled = false; + this.btnActive = true; + } + if (changedProperties.has('text') && this.text.length > 0) { const commentsInput = this.commentInput; commentsInput.value = this.text; diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index e93b7524..5a7f58f7 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -175,6 +175,7 @@ export class CommentsCommentItem extends WebComponentsBaseElement { event.stopPropagation()} text=${this.text} eventType="update-comment" From 802caafb8485679d4255ed2bd11566d6a9095a35 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 4 Apr 2024 14:13:35 -0300 Subject: [PATCH 56/83] fix: remove check to local participant on wio list --- src/components/who-is-online/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index cb3732ae..c43b8335 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -139,7 +139,7 @@ export class WhoIsOnline extends BaseComponent { */ private onParticipantListUpdate = (data: Record): void => { const updatedParticipants = Object.values(data).filter(({ data }) => { - return data.activeComponents?.includes('whoIsOnline') || data.id === this.localParticipantId; + return data.activeComponents?.includes('whoIsOnline'); }); const participants = updatedParticipants From 332c94a70e89a3a47e746dca9c6f7a2f2689811e Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 4 Apr 2024 14:14:35 -0300 Subject: [PATCH 57/83] fix: only publish joined event to participants that joins after local participant --- src/core/launcher/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 174cd155..5fc02dcc 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -329,6 +329,12 @@ export class Launcher extends Observable implements DefaultLauncher { private startIOC = (): void => { this.logger.log('launcher service @ startIOC'); + // retrieve the current participants in the room + this.LauncherRealtimeRoom.presence.get((presences) => { + presences.forEach((presence) => { + this.participants.value.set(presence.id, presence.data as Participant); + }); + }); this.LauncherRealtimeRoom.presence.on( Socket.PresenceEvents.JOINED_ROOM, @@ -371,6 +377,7 @@ export class Launcher extends Observable implements DefaultLauncher { this.logger.log('launcher service @ onParticipantJoined - local participant joined'); this.publish(ParticipantEvent.LOCAL_JOINED, this.participant.value); + this.publish(ParticipantEvent.JOINED, this.participant.value); }; /** From e6ce2f63450008565daa815b91593c60035bcdf9 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 4 Apr 2024 14:15:21 -0300 Subject: [PATCH 58/83] fix: keep active components updates on old ioc --- src/core/launcher/index.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 5fc02dcc..4daa15a6 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -298,6 +298,17 @@ export class Launcher extends Observable implements DefaultLauncher { if (localParticipant && !isEqual(this.participant.value, localParticipant)) { this.LauncherRealtimeRoom.presence.update(localParticipant); + + 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 (!localParticipant.activeComponents) return true; + + return this.activeComponents.includes(component.name); + }); } }; @@ -329,6 +340,7 @@ export class Launcher extends Observable implements DefaultLauncher { private startIOC = (): void => { this.logger.log('launcher service @ startIOC'); + // retrieve the current participants in the room this.LauncherRealtimeRoom.presence.get((presences) => { presences.forEach((presence) => { @@ -409,17 +421,6 @@ export class Launcher extends Observable implements DefaultLauncher { presence.id === this.participant.value.id && !isEqual(this.participant.value, presence.data) ) { - 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 (!presence.data.activeComponents) return true; - - return this.activeComponents.includes(component.name); - }); - this.participant.value = presence.data; this.publish(ParticipantEvent.LOCAL_UPDATED, presence.data); From c404c6b70585597ad20c715038e2d1453941bcb0 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Thu, 4 Apr 2024 14:44:31 -0300 Subject: [PATCH 59/83] feat: validates if realtime is ready before fetch history --- src/components/realtime/index.test.ts | 7 +++++++ src/components/realtime/index.ts | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/realtime/index.test.ts b/src/components/realtime/index.test.ts index 90262fd3..d6dd7d7a 100644 --- a/src/components/realtime/index.test.ts +++ b/src/components/realtime/index.test.ts @@ -135,6 +135,13 @@ describe('realtime component', () => { }); describe('fetchHistory', () => { + test('should return null when the realtime is not started', async () => { + RealtimeComponentInstance['state'] = RealtimeComponentState.STOPPED; + const h = await RealtimeComponentInstance.fetchHistory(); + + expect(h).toEqual(null); + }); + test('should return null when the history is empty', async () => { const spy = jest .spyOn(RealtimeComponentInstance['room'], 'history' as any) diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index 03e881da..63225f29 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -59,7 +59,7 @@ export class Realtime extends BaseComponent { if (this.state !== RealtimeComponentState.STARTED) { const message = `Realtime component is not started yet. You can't publish event ${event} before start`; this.logger.log(message); - console.error(message); + console.warn(`[SuperViz] ${message}`); return; } @@ -107,6 +107,14 @@ export class Realtime extends BaseComponent { public fetchHistory = async ( eventName?: string, ): Promise | null> => { + if (this.state !== RealtimeComponentState.STARTED) { + const message = `Realtime component is not started yet. You can't retrieve history before start`; + + this.logger.log(message); + console.warn(`[SuperViz] ${message}`); + return null; + } + const history: RealtimeMessage[] | Record = await new Promise( (resolve, reject) => { const next = (data: Socket.RoomHistory) => { From 05b4f3de3e173e49544d7c9eb49a65703b4d5aa5 Mon Sep 17 00:00:00 2001 From: vitorvargasdev Date: Thu, 4 Apr 2024 18:55:29 -0300 Subject: [PATCH 60/83] fix: resolve icon --- src/web-components/comments/components/comment-item.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index 5a7f58f7..8d0d15a3 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -242,7 +242,6 @@ export class CommentsCommentItem extends WebComponentsBaseElement { Date: Thu, 4 Apr 2024 19:18:17 -0300 Subject: [PATCH 61/83] fix: add word break in edit mode --- src/web-components/comments/css/comment-item.style.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web-components/comments/css/comment-item.style.ts b/src/web-components/comments/css/comment-item.style.ts index d0a71afc..6537cf53 100644 --- a/src/web-components/comments/css/comment-item.style.ts +++ b/src/web-components/comments/css/comment-item.style.ts @@ -66,6 +66,7 @@ export const commentItemStyle = css` .comments__comment-item__content { width: 100%; + word-wrap: break-word; } .line-clamp { From 213e1f85635e219f669338e294d91d7a284cc3c4 Mon Sep 17 00:00:00 2001 From: vitorvargasdev Date: Thu, 4 Apr 2024 19:31:50 -0300 Subject: [PATCH 62/83] fix: hidden class --- src/web-components/comments/css/annotation-item.style.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-components/comments/css/annotation-item.style.ts b/src/web-components/comments/css/annotation-item.style.ts index ff70d61a..fecb34c2 100644 --- a/src/web-components/comments/css/annotation-item.style.ts +++ b/src/web-components/comments/css/annotation-item.style.ts @@ -82,6 +82,7 @@ export const annotationItemStyle = css` .hidden { overflow: hidden; + opacity: 0; } .comments__thread { @@ -111,7 +112,6 @@ export const annotationItemStyle = css` grid-template-rows: 0fr; opacity: 0; width: 100%; - overflow: hidden; transition: grid-template-rows 0.3s linear, opacity 0.3s linear; } From 2dd4322015fbfc103b989ce79d61ef70d2ea87d5 Mon Sep 17 00:00:00 2001 From: vitorvargasdev Date: Thu, 4 Apr 2024 19:39:58 -0300 Subject: [PATCH 63/83] fix: change resolve icon to md temporary --- src/web-components/comments/components/comment-item.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index 8d0d15a3..a61e1a19 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -239,9 +239,10 @@ export class CommentsCommentItem extends WebComponentsBaseElement { 'resolve-icon', )} icon-button icon-button--clickable icon-button--xsmall ${isResolvable}" > + Date: Thu, 4 Apr 2024 19:41:27 -0300 Subject: [PATCH 64/83] refactor: remove unnecessary test --- src/web-components/comments/comments.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/web-components/comments/comments.test.ts b/src/web-components/comments/comments.test.ts index dde42b77..4e3b7938 100644 --- a/src/web-components/comments/comments.test.ts +++ b/src/web-components/comments/comments.test.ts @@ -63,11 +63,6 @@ describe('comments', () => { expect(app?.classList.contains('superviz-comments')).toBe(true); }); - // FIXME: Need refactor should listen event toggle - test('should toggle superviz comments', async () => { - // ! WIP ! - }); - test('should update annotations', async () => { const annotations = [{ id: '1', x: 0, y: 0, width: 0, height: 0, text: 'test' }]; From 1c8760550ae14e39966db695d47d82ddb9b22462 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Fri, 5 Apr 2024 07:54:26 -0300 Subject: [PATCH 65/83] fix: use only the last message from the last hour --- src/services/realtime/ably/index.test.ts | 3 +-- src/services/realtime/ably/index.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/services/realtime/ably/index.test.ts b/src/services/realtime/ably/index.test.ts index 8a58e95f..f85e8141 100644 --- a/src/services/realtime/ably/index.test.ts +++ b/src/services/realtime/ably/index.test.ts @@ -433,7 +433,7 @@ describe('AblyRealtimeService', () => { expect(AblyRealtimeServiceInstance['isJoinedRoom']).toBe(true); expect(AblyRealtimeServiceInstance['fetchRoomProperties']).toHaveBeenCalledTimes(2); - expect(AblyRealtimeServiceInstance['updateParticipants']).toHaveBeenCalledTimes(1); + expect(AblyRealtimeServiceInstance['updateParticipants']).toHaveBeenCalledTimes(2); expect(AblyRealtimeServiceInstance['updateLocalRoomState']).toHaveBeenCalledTimes(1); expect(AblyRealtimeServiceInstance['publishStateUpdate']).toHaveBeenCalledWith( RealtimeStateTypes.CONNECTED, @@ -471,7 +471,6 @@ describe('AblyRealtimeService', () => { expect(AblyRealtimeServiceInstance['isJoinedRoom']).toBe(true); expect(AblyRealtimeServiceInstance['fetchRoomProperties']).toHaveBeenCalledTimes(1); - expect(AblyRealtimeServiceInstance['updateParticipants']).not.toBeCalled(); expect(AblyRealtimeServiceInstance['updateLocalRoomState']).not.toBeCalled(); expect(AblyRealtimeServiceInstance['publishStateUpdate']).toHaveBeenCalledWith( RealtimeStateTypes.CONNECTED, diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 47fa6303..5d45a305 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -298,6 +298,7 @@ export default class AblyRealtimeService extends RealtimeService implements Ably */ public setTranscript(state: TranscriptState): void { const roomProperties = this.localRoomProperties; + this.updateRoomProperties(Object.assign({}, roomProperties, { transcript: state })); } @@ -624,6 +625,8 @@ export default class AblyRealtimeService extends RealtimeService implements Ably followParticipantId: null, gather: false, drawing: null, + transcript: TranscriptState.TRANSCRIPT_STOP, + kickParticipant: null, }; this.updateParticipants(); @@ -696,14 +699,14 @@ export default class AblyRealtimeService extends RealtimeService implements Ably * @function fetchRoomProperties * @returns {AblyRealtimeData | null} */ - private fetchRoomProperties(): Promise { - return new Promise((resolve, reject) => { + private async fetchRoomProperties(): Promise { + const lastMessage: Ably.Types.Message | null = await new Promise((resolve, reject) => { this.supervizChannel.history((error, resultPage) => { if (error) { reject(error); } - const lastMessage = resultPage.items[0]?.data; + const lastMessage = resultPage.items[0]; if (lastMessage) { resolve(lastMessage); @@ -712,6 +715,10 @@ export default class AblyRealtimeService extends RealtimeService implements Ably } }); }); + + if (lastMessage?.timestamp < Date.now() - 1000 * 60 * 60) return null; + + return lastMessage?.data || null; } /** @@ -802,6 +809,7 @@ export default class AblyRealtimeService extends RealtimeService implements Ably ): Promise { this.isJoinedRoom = true; this.localRoomProperties = await this.fetchRoomProperties(); + this.updateParticipants(); this.myParticipant = myPresence; if (!this.localRoomProperties) { From 646b650e5f60efd3e2a2c4c4ffdb4464cb7f580b Mon Sep 17 00:00:00 2001 From: vitorvargasdev Date: Fri, 5 Apr 2024 10:33:35 -0300 Subject: [PATCH 66/83] fix: remove hover in confirm button --- src/web-components/comments/components/comment-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-components/comments/components/comment-input.ts b/src/web-components/comments/components/comment-input.ts index e1aea337..0d4bc44a 100644 --- a/src/web-components/comments/components/comment-input.ts +++ b/src/web-components/comments/components/comment-input.ts @@ -393,7 +393,7 @@ export class CommentsCommentInput extends WebComponentsBaseElement { Date: Fri, 5 Apr 2024 15:33:11 -0300 Subject: [PATCH 68/83] feat: create who is online store --- src/services/stores/who-is-online/index.ts | 45 ++++++++++++++++++++++ src/services/stores/who-is-online/types.ts | 5 +++ 2 files changed, 50 insertions(+) create mode 100644 src/services/stores/who-is-online/types.ts diff --git a/src/services/stores/who-is-online/index.ts b/src/services/stores/who-is-online/index.ts index db2bd93a..271cf65a 100644 --- a/src/services/stores/who-is-online/index.ts +++ b/src/services/stores/who-is-online/index.ts @@ -1,11 +1,28 @@ +import { Participant } from '../../../components/who-is-online/types'; import { Singleton } from '../common/types'; import { CreateSingleton } from '../common/utils'; import subject from '../subject'; +import { Following } from './types'; + const instance: Singleton = CreateSingleton(); export class WhoIsOnlineStore { public disablePresenceControls = subject(false); + public disableGoToParticipant = subject(false); + public disableFollowParticipant = subject(false); + public disablePrivateMode = subject(false); + public disableGatherAll = subject(false); + public disableFollowMe = subject(false); + + public participants = subject([]); + public extras = subject([]); + + public joinedPresence = subject(undefined); + public everyoneFollowsMe = subject(false); + public privateMode = subject(false); + + public following = subject(undefined); constructor() { if (instance.value) { @@ -25,10 +42,38 @@ const store = new WhoIsOnlineStore(); const destroy = store.destroy.bind(store) as () => void; const disablePresenceControls = store.disablePresenceControls.expose(); +const disableGoToParticipant = store.disableGoToParticipant.expose(); +const disableFollowParticipant = store.disableFollowParticipant.expose(); +const disablePrivateMode = store.disablePrivateMode.expose(); +const disableGatherAll = store.disableGatherAll.expose(); +const disableFollowMe = store.disableFollowMe.expose(); +const joinedPresence = store.joinedPresence.expose(); +const participants = store.participants.expose(); +const extras = store.extras.expose(); + +const everyoneFollowsMe = store.everyoneFollowsMe.expose(); +const privateMode = store.privateMode.expose(); + +const following = store.following.expose(); export function useWhoIsOnlineStore() { return { disablePresenceControls, + disableGoToParticipant, + disableFollowParticipant, + disablePrivateMode, + disableGatherAll, + disableFollowMe, + + participants, + extras, + + joinedPresence, + everyoneFollowsMe, + privateMode, + + following, + destroy, }; } diff --git a/src/services/stores/who-is-online/types.ts b/src/services/stores/who-is-online/types.ts new file mode 100644 index 00000000..10128f50 --- /dev/null +++ b/src/services/stores/who-is-online/types.ts @@ -0,0 +1,5 @@ +export interface Following { + id: string; + name: string; + color: string; +} From 98641920f84dcc2541a60c919e828934e82b057f Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Apr 2024 15:34:22 -0300 Subject: [PATCH 69/83] fix: revise useStore function usability --- src/common/types/stores.types.ts | 1 + src/common/utils/use-store.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/common/types/stores.types.ts b/src/common/types/stores.types.ts index 43af99a2..01cf29f1 100644 --- a/src/common/types/stores.types.ts +++ b/src/common/types/stores.types.ts @@ -13,6 +13,7 @@ type StoreApi any> = { subscribe(callback?: (value: keyof T) => void): void; subject: PublicSubject; publish(value: T): void; + value: any; }; }; diff --git a/src/common/utils/use-store.ts b/src/common/utils/use-store.ts index 90e20c56..85fc90f4 100644 --- a/src/common/utils/use-store.ts +++ b/src/common/utils/use-store.ts @@ -49,6 +49,9 @@ export function useStore(name: T): Store { bindedSubscribeTo(valueName, store[valueName], callback); }, subject: store[valueName] as typeof storeData, + get value() { + return this.subject.value; + }, publish(newValue: keyof Store) { this.subject.value = newValue; }, From 81f66d1a250be61a735c5085e1f174c2b60643d1 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Apr 2024 15:36:04 -0300 Subject: [PATCH 70/83] feat: separate computation logic and render logic, concentrate informations in wio store --- src/components/who-is-online/index.ts | 315 +++++++++++--- src/components/who-is-online/types.ts | 52 ++- src/web-components/dropdown/index.test.ts | 4 +- src/web-components/dropdown/index.ts | 58 ++- src/web-components/dropdown/types.ts | 5 + src/web-components/tooltip/index.ts | 6 +- .../who-is-online/components/dropdown.ts | 93 ++--- .../who-is-online/components/messages.ts | 10 +- .../who-is-online/components/types.ts | 23 -- .../who-is-online/who-is-online.ts | 387 ++++++++---------- 10 files changed, 568 insertions(+), 385 deletions(-) create mode 100644 src/web-components/dropdown/types.ts diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index cb3732ae..6e89e473 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -1,22 +1,32 @@ +import { PresenceEvents } from '@superviz/socket-client'; import { isEqual } from 'lodash'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; +import { Avatar } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; import { Logger } from '../../common/utils'; import { AblyParticipant } from '../../services/realtime/ably/types'; +import { Following } from '../../services/stores/who-is-online/types'; import { WhoIsOnline as WhoIsOnlineElement } from '../../web-components'; +import { DropdownOption } from '../../web-components/dropdown/types'; import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; -import { WhoIsOnlinePosition, Position, Participant, WhoIsOnlineOptions } from './types'; +import { + WhoIsOnlinePosition, + Position, + Participant, + WhoIsOnlineOptions, + TooltipData, + WIODropdownOptions, +} from './types'; export class WhoIsOnline extends BaseComponent { public name: ComponentNames; protected logger: Logger; private element: WhoIsOnlineElement; private position: WhoIsOnlinePosition; - private participants: Participant[] = []; - private following: string; + private following: Following; private localParticipantId: string; constructor(options?: WhoIsOnlinePosition | WhoIsOnlineOptions) { @@ -33,8 +43,22 @@ export class WhoIsOnline extends BaseComponent { this.position = options.position ?? Position.TOP_RIGHT; this.setStyles(options.styles); - const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + const { + disablePresenceControls, + disableGoToParticipant, + disableFollowParticipant, + disablePrivateMode, + disableGatherAll, + disableFollowMe, + extras, + } = this.useStore(StoreType.WHO_IS_ONLINE); + disablePresenceControls.publish(options.flags?.disablePresenceControls); + disableGoToParticipant.publish(options.flags?.disableGoToParticipant); + disableFollowParticipant.publish(options.flags?.disableFollowParticipant); + disablePrivateMode.publish(options.flags?.disablePrivateMode); + disableGatherAll.publish(options.flags?.disableGatherAll); + disableFollowMe.publish(options.flags?.disableFollowMe); } /** @@ -44,7 +68,7 @@ export class WhoIsOnline extends BaseComponent { */ protected start(): void { const { localParticipant } = this.useStore(StoreType.GLOBAL); - localParticipant.subscribe((value: Participant) => { + localParticipant.subscribe((value: { id: string }) => { this.localParticipantId = value.id; }); @@ -52,7 +76,7 @@ export class WhoIsOnline extends BaseComponent { this.positionWhoIsOnline(); this.addListeners(); - this.realtime.enterWIOChannel(localParticipant.subject.value as Participant); + this.realtime.enterWIOChannel(localParticipant.value); } /** @@ -66,7 +90,6 @@ export class WhoIsOnline extends BaseComponent { this.removeListeners(); this.element.remove(); this.element = null; - this.participants = null; } /** @@ -102,6 +125,7 @@ export class WhoIsOnline extends BaseComponent { this.element.removeEventListener(RealtimeEvent.REALTIME_PRIVATE_MODE, this.setPrivate); this.element.removeEventListener(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, this.follow); this.element.removeEventListener(RealtimeEvent.REALTIME_GATHER, this.gather); + this.room.presence.off(PresenceEvents.UPDATE); } /** @@ -138,32 +162,14 @@ export class WhoIsOnline extends BaseComponent { * @returns {void} */ private onParticipantListUpdate = (data: Record): void => { - const updatedParticipants = Object.values(data).filter(({ data }) => { - return data.activeComponents?.includes('whoIsOnline') || data.id === this.localParticipantId; - }); - - const participants = updatedParticipants - .filter(({ data: { isPrivate, id } }) => { - return !isPrivate || (isPrivate && id === this.localParticipantId); - }) - .map(({ data }) => { - const { slotIndex, id, name, avatar, activeComponents } = data as Participant; - const { color } = this.realtime.getSlotColor(slotIndex); - const isLocal = this.localParticipantId === id; - const joinedPresence = activeComponents.some((component) => component.includes('presence')); - - return { name, id, slotIndex, color, isLocal, joinedPresence, avatar }; - }); + const updatedParticipants = this.filterParticipants(Object.values(data)); - if (isEqual(participants, this.participants)) return; - - if (this.following) { - const participantBeingFollowed = participants.find(({ id }) => id === this.following); - if (!participantBeingFollowed) this.stopFollowing({ clientId: this.following }); - } + const mappedParticipants = updatedParticipants.map((participant) => + this.getParticipant(participant), + ); - this.participants = participants; - this.element.updateParticipants(this.participants); + const remainingParticipants = this.setParticipants(mappedParticipants); + this.setExtras(remainingParticipants); }; /** @@ -232,14 +238,32 @@ export class WhoIsOnline extends BaseComponent { */ private followMousePointer = ({ detail }: CustomEvent) => { this.eventBus.publish(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, detail.id); - this.following = detail.id; + + const { participants, following } = this.useStore(StoreType.WHO_IS_ONLINE); if (this.following) { this.publish(WhoIsOnlineEvent.START_FOLLOWING_PARTICIPANT, this.following); - return; } - this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); + if (!this.following) { + this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); + } + + participants.publish( + participants.value.map((participant: Participant) => { + const participantId = participant.id; + const disableDropdown = this.shouldDisableDropdown({ + activeComponents: participant.activeComponents, + }); + const presenceEnabled = !disableDropdown; + const controls = this.getControls({ participantId, presenceEnabled }) ?? []; + + return { + ...participant, + controls, + }; + }), + ); }; /** @@ -249,48 +273,68 @@ export class WhoIsOnline extends BaseComponent { * @returns {void} */ private setPrivate = ({ detail: { isPrivate, id } }: CustomEvent) => { + const { privateMode } = this.useStore(StoreType.WHO_IS_ONLINE); + privateMode.publish(isPrivate); + this.eventBus.publish(RealtimeEvent.REALTIME_PRIVATE_MODE, isPrivate); this.realtime.setPrivateWIOParticipant(id, isPrivate); if (isPrivate) { this.publish(WhoIsOnlineEvent.ENTER_PRIVATE_MODE); - return; } - this.publish(WhoIsOnlineEvent.LEAVE_PRIVATE_MODE); + if (!isPrivate) { + this.publish(WhoIsOnlineEvent.LEAVE_PRIVATE_MODE); + } }; - private setFollow = (following) => { - if (following.clientId === this.localParticipantId) return; + private setFollow = (followingData) => { + if (followingData.clientId === this.localParticipantId) return; - this.followMousePointer({ detail: { id: following?.data?.id } } as CustomEvent); + this.followMousePointer({ detail: { id: followingData?.data?.id } } as CustomEvent); + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); - if (!following.data.id) { - this.element.following = undefined; + if (!followingData.data.id) { + following.publish(undefined); return; } - this.following = following.data.id; - this.element.following = following.data; + following.publish(followingData.data); }; private follow = (data: CustomEvent) => { + const { everyoneFollowsMe, participants } = this.useStore(StoreType.WHO_IS_ONLINE); + everyoneFollowsMe.publish(!!data.detail?.id); + this.realtime.setFollowWIOParticipant({ ...data.detail }); - this.following = data.detail?.id; if (this.following) { this.publish(WhoIsOnlineEvent.START_FOLLOW_ME, this.following); - return; } - this.publish(WhoIsOnlineEvent.STOP_FOLLOW_ME); + if (!this.following) { + this.publish(WhoIsOnlineEvent.STOP_FOLLOW_ME); + } + + participants.publish( + participants.value.map((participant) => { + if (participant.id !== this.localParticipantId) return participant; + + const controls = this.getLocalParticipantControls(); + return { + ...participant, + controls, + }; + }), + ); }; private stopFollowing = (participant: { clientId: string }) => { - if (participant.clientId !== this.element.following?.id) return; + if (participant.clientId !== this.following?.id) return; + + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + following.publish(undefined); - this.element.following = undefined; - this.following = undefined; this.eventBus.publish(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, undefined); this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); }; @@ -299,4 +343,177 @@ export class WhoIsOnline extends BaseComponent { this.realtime.setGatherWIOParticipant({ ...data.detail }); this.publish(WhoIsOnlineEvent.GATHER_ALL, data.detail.id); }; + + private filterParticipants(participants: AblyParticipant[]) { + return participants.filter(({ data: { activeComponents, id, isPrivate } }) => { + if (isPrivate && this.localParticipantId !== id) return false; + + const isLocal = id === this.localParticipantId; + + if (isLocal) { + const hasPresenceComponent = activeComponents?.some((component) => + component.includes('presence'), + ); + const { joinedPresence } = this.useStore(StoreType.WHO_IS_ONLINE); + joinedPresence.publish(hasPresenceComponent); + } + + return activeComponents?.includes('whoIsOnline') || isLocal; + }); + } + + private getParticipant(participant: AblyParticipant): Participant { + const { avatar: avatarLinks, activeComponents, participantId, name } = participant.data; + const isLocalParticipant = participant.clientId === this.localParticipantId; + + const { color } = this.realtime.getSlotColor(participant.data.slotIndex); + const disableDropdown = this.shouldDisableDropdown({ activeComponents }); + const presenceEnabled = !disableDropdown; + + const tooltip = this.getTooltipData({ isLocalParticipant, name, presenceEnabled }); + const avatar = this.getAvatar({ avatar: avatarLinks, color, name }); + const controls = this.getControls({ participantId, presenceEnabled }) ?? []; + + return { + id: participantId, + name, + avatar, + disableDropdown, + tooltip, + controls, + activeComponents, + isLocalParticipant, + }; + } + + private shouldDisableDropdown({ activeComponents }: { activeComponents: string[] | undefined }) { + const { joinedPresence, disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + if (joinedPresence.value === false || disablePresenceControls.value === true) return true; + + return !activeComponents?.some((component) => component.toLowerCase().includes('presence')); + } + + private getTooltipData({ + isLocalParticipant, + name, + presenceEnabled, + }: { + isLocalParticipant: boolean; + name: string; + presenceEnabled: boolean; + }): TooltipData { + const data: TooltipData = { name }; + + if (isLocalParticipant) { + data.name += ' (You)'; + } + + if (presenceEnabled && !isLocalParticipant) { + data.info = 'Click to follow'; + } + + return data; + } + + private getAvatar({ avatar, color, name }: { avatar: Avatar; name: string; color: string }) { + const imageUrl = avatar?.imageUrl; + const firstLetter = name?.at(0).toUpperCase() ?? 'A'; + + return { imageUrl, firstLetter, color }; + } + + private getControls({ + participantId, + presenceEnabled, + }: { + participantId: string; + presenceEnabled: boolean; + }): DropdownOption[] | undefined { + const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); + + if (disablePresenceControls.value || !presenceEnabled) return; + + if (participantId === this.localParticipantId) { + return this.getLocalParticipantControls(); + } + + return this.getOtherParticipantsControls(participantId); + } + + private getOtherParticipantsControls(participantId: string): DropdownOption[] { + const { disableGoToParticipant, disableFollowParticipant, following } = this.useStore( + StoreType.WHO_IS_ONLINE, + ); + + const controls: DropdownOption[] = []; + + if (!disableGoToParticipant.value) { + controls.push({ option: WIODropdownOptions['GOTO'], icon: 'place' }); + } + + if (!disableFollowParticipant.value) { + const isBeingFollowed = following.value?.id === participantId; + const option = isBeingFollowed + ? WIODropdownOptions['LOCAL_UNFOLLOW'] + : WIODropdownOptions['LOCAL_FOLLOW']; + const icon = isBeingFollowed ? 'send-off' : 'send'; + controls.push({ option, icon }); + } + + return controls; + } + + private getLocalParticipantControls(): DropdownOption[] { + const { + disableFollowMe: { value: disableFollowMe }, + disableGatherAll: { value: disableGatherAll }, + disablePrivateMode: { value: disablePrivateMode }, + everyoneFollowsMe: { value: everyoneFollowsMe }, + privateMode: { value: privateMode }, + } = this.useStore(StoreType.WHO_IS_ONLINE); + + const controls: DropdownOption[] = []; + + if (!disableGatherAll) { + controls.push({ option: WIODropdownOptions['GATHER'], icon: 'gather' }); + } + + if (!disableFollowMe) { + const icon = everyoneFollowsMe ? 'send-off' : 'send'; + const option = everyoneFollowsMe + ? WIODropdownOptions['UNFOLLOW'] + : WIODropdownOptions['FOLLOW']; + + controls.push({ option, icon }); + } + + if (!disablePrivateMode) { + const icon = privateMode ? 'eye_inative' : 'eye'; + const option = privateMode + ? WIODropdownOptions['LEAVE_PRIVATE'] + : WIODropdownOptions['PRIVATE']; + + controls.push({ option, icon }); + } + + return controls; + } + + private setParticipants = (participantsList: Participant[]): Participant[] => { + const { participants } = this.useStore(StoreType.WHO_IS_ONLINE); + + const localParticipantIndex = participantsList.findIndex(({ id }) => { + return id === this.localParticipantId; + }); + + const localParticipant = participantsList.splice(localParticipantIndex, 1); + const otherParticipants = participantsList.splice(0, 3); + participants.publish([...localParticipant, ...otherParticipants]); + return participantsList; + }; + + private setExtras = (participantsList: Participant[]) => { + const { extras } = this.useStore(StoreType.WHO_IS_ONLINE); + extras.publish(participantsList); + }; } diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 17c41de7..06d1926c 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -1,4 +1,5 @@ import { Participant as GeneralParticipant } from '../../common/types/participant.types'; +import { DropdownOption } from '../../web-components/dropdown/types'; export enum Position { TOP_LEFT = 'top-left', @@ -7,13 +8,35 @@ export enum Position { BOTTOM_RIGHT = 'bottom-right', } -export interface Participant extends GeneralParticipant { - slotIndex?: number; - isLocal?: boolean; - joinedPresence?: boolean; - isPrivate?: boolean; +export interface TooltipData { + name: string; + info?: string; } +export interface Avatar { + imageUrl: string; + firstLetter?: string; + color: string; +} + +export interface Participant { + id: string; + name: string; + disableDropdown?: boolean; + controls?: DropdownOption[]; + tooltip: TooltipData; + avatar: Avatar; + activeComponents: string[]; + isLocalParticipant: boolean; + // beingFollowed?: boolean; + // slotIndex?: number; + // make go immediately to the mouse when following +} + +// refactor everywhere that uses superviz-dropdown because of icon changes +// and label removed too +// create tooltip inside WIO store + export type WhoIsOnlinePosition = Position | `${Position}` | string | ''; export interface WhoIsOnlineOptions { @@ -21,5 +44,22 @@ export interface WhoIsOnlineOptions { styles?: string; flags?: { disablePresenceControls?: boolean; - } + disableGoToParticipant?: boolean; + disableFollowParticipant?: boolean; + disablePrivateMode?: boolean; + disableGatherAll?: boolean; + disableFollowMe?: boolean; + }; +} + +export enum WIODropdownOptions { + GOTO = 'go to', + LOCAL_FOLLOW = 'follow', + LOCAL_UNFOLLOW = 'unfollow', + FOLLOW = 'everyone follows me', + UNFOLLOW = 'stop followers', + PRIVATE = 'private mode', + LEAVE_PRIVATE = 'leave private mode', + GATHER = 'gather all', + STOP_GATHER = 'stop gather all', } diff --git a/src/web-components/dropdown/index.test.ts b/src/web-components/dropdown/index.test.ts index 4e24ab52..691dbb85 100644 --- a/src/web-components/dropdown/index.test.ts +++ b/src/web-components/dropdown/index.test.ts @@ -46,11 +46,11 @@ export const createEl = ({ } if (showTooltip) { - const onHoverData = { + const tooltipData = { name: 'onHover', action: 'Click to see more', }; - element.setAttribute('onHoverData', JSON.stringify(onHoverData)); + element.setAttribute('tooltipData', JSON.stringify(tooltipData)); element.setAttribute('canShowTooltip', 'true'); } diff --git a/src/web-components/dropdown/index.ts b/src/web-components/dropdown/index.ts index 8a7c3808..7057dffb 100644 --- a/src/web-components/dropdown/index.ts +++ b/src/web-components/dropdown/index.ts @@ -6,6 +6,7 @@ import { WebComponentsBase } from '../base'; import importStyle from '../base/utils/importStyle'; import { dropdownStyle } from './index.style'; +import { DropdownOption } from './types'; const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, dropdownStyle]; @@ -17,19 +18,21 @@ export class Dropdown extends WebComponentsBaseElement { declare open: boolean; declare disabled: boolean; declare align: 'left' | 'right'; - declare options: object[]; - declare label: string; + declare options: DropdownOption[]; + declare returnData: { [k: string]: any }; + declare returnTo: string; declare active: string | object; declare icons?: string[]; declare name?: string; - declare onHoverData: { name: string; action: string }; + declare tooltipData: { name: string; action: string }; declare shiftTooltipLeft: boolean; declare lastParticipant: boolean; declare classesPrefix: string; declare parentComponent: string; declare tooltipPrefix: string; declare dropdown: HTMLElement; + // true when the dropdown is hovered (pass to tooltip element) declare showTooltip: boolean; // true if the tooltip should be shown when hovering (use in this element) @@ -43,12 +46,11 @@ export class Dropdown extends WebComponentsBaseElement { disabled: { type: Boolean }, align: { type: String }, options: { type: Array }, - label: { type: String }, returnTo: { type: String }, active: { type: [String, Object] }, icons: { type: Array }, name: { type: String }, - onHoverData: { type: Object }, + tooltipData: { type: Object }, showTooltip: { type: Boolean }, dropdown: { type: Object }, canShowTooltip: { type: Boolean }, @@ -58,11 +60,13 @@ export class Dropdown extends WebComponentsBaseElement { classesPrefix: { type: String }, parentComponent: { type: String }, tooltipPrefix: { type: String }, + returnData: { type: Object }, }; constructor() { super(); this.showTooltip = false; + this.returnData = {}; } protected firstUpdated( @@ -132,15 +136,17 @@ export class Dropdown extends WebComponentsBaseElement { }); }; - private callbackSelected = (option) => { + private callbackSelected = ({ option }: DropdownOption) => { this.open = false; - const returnTo = this.returnTo ? option[this.returnTo] : option; - - this.emitEvent('selected', returnTo, { - bubbles: false, - composed: true, - }); + this.emitEvent( + 'selected', + { ...this.returnData, label: option }, + { + bubbles: false, + composed: true, + }, + ); }; private setHorizontalPosition() { @@ -223,14 +229,22 @@ export class Dropdown extends WebComponentsBaseElement { this.emitEvent('open', { open: this.open }); } - private get supervizIcons() { - return this.icons?.map((icon) => { - return html``; - }); + private getIcon(icon: string) { + if (!icon) return; + + return html` + + + + `; + } + + private getLabel(option: string) { + return html`${option}`; } private get listOptions() { - return this.options.map((option, index) => { + return this.options.map(({ option, icon, data }) => { const liClasses = { text: true, [this.getClass('item')]: true, @@ -238,9 +252,11 @@ export class Dropdown extends WebComponentsBaseElement { active: option?.[this.returnTo] && this.active === option?.[this.returnTo], }; - return html`
  • this.callbackSelected(option)} class=${classMap(liClasses)}> - ${this.supervizIcons?.at(index)} - ${option[this.label]} + return html`
  • this.callbackSelected({ option, data })} + class=${classMap(liClasses)} + > + ${this.getIcon(icon)} ${this.getLabel(option)}
  • `; }); } @@ -251,7 +267,7 @@ export class Dropdown extends WebComponentsBaseElement { const tooltipVerticalPosition = this.lastParticipant ? 'tooltip-top' : 'tooltip-bottom'; return html`

    ${this.tooltipData?.name}

    - ${this.tooltipData?.action - ? html`

    ${this.tooltipData?.action}

    ` + ${this.tooltipData?.info + ? html`

    ${this.tooltipData?.info}

    ` : ''}
    `; diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 3a97c18e..264cfe78 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { CSSResultGroup, LitElement, PropertyValueMap, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -5,12 +6,12 @@ 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 { Participant } from '../../../components/who-is-online/types'; +import { Avatar, Participant } from '../../../components/who-is-online/types'; import { WebComponentsBase } from '../../base'; import importStyle from '../../base/utils/importStyle'; import { dropdownStyle } from '../css'; -import { Following, WIODropdownOptions, TooltipData, VerticalSide, HorizontalSide } from './types'; +import { Following, VerticalSide, HorizontalSide } from './types'; const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, dropdownStyle]; @@ -22,26 +23,22 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { declare open: boolean; declare align: HorizontalSide; declare position: VerticalSide; - declare participants: Participant[]; declare selected: string; declare disableDropdown: boolean; declare showSeeMoreTooltip: boolean; declare showParticipantTooltip: boolean; - declare following: Following; declare localParticipantJoinedPresence: boolean; declare dropdownList: HTMLElement; + private participants: Participant[]; private animationFrame: number; - private disablePresenceControls: boolean; static properties = { open: { type: Boolean }, align: { type: String }, position: { type: String }, - participants: { type: Array }, selected: { type: String }, disableDropdown: { type: Boolean }, - following: { type: Object }, showSeeMoreTooltip: { type: Boolean }, showParticipantTooltip: { type: Boolean }, localParticipantJoinedPresence: { type: Boolean }, @@ -53,8 +50,10 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { this.selected = ''; this.showParticipantTooltip = true; - const { disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); - disablePresenceControls.subscribe(); + const { extras } = this.useStore(StoreType.WHO_IS_ONLINE); + extras.subscribe((participants) => { + this.participants = participants; + }); } protected firstUpdated( @@ -116,22 +115,23 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { }; }; - private getAvatar(participant: Participant) { - if (participant.avatar?.imageUrl) { + private getAvatar({ color, imageUrl, firstLetter }: Avatar) { + if (imageUrl) { return html` `; } - const letterColor = INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : '#26242A'; + const letterColor = + /* INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : */ '#26242A'; return html`
    - ${participant.name?.at(0).toUpperCase()} + ${firstLetter}
    `; } @@ -146,22 +146,18 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { private renderParticipants() { if (!this.participants) return; - const icons = ['place', 'send']; const numberOfParticipants = this.participants.length - 1; return repeat( this.participants, (participant) => participant.id, (participant, index) => { - const { id, slotIndex, joinedPresence, isLocal, color, name } = participant; - const disableDropdown = - !joinedPresence || isLocal || this.disableDropdown || this.disablePresenceControls; + const { disableDropdown, id, avatar, controls, tooltip, name } = participant; const contentClasses = { 'who-is-online__extra-participant': true, 'who-is-online__extra-participant--selected': this.selected === id, 'disable-dropdown': disableDropdown, - followed: this.following?.id === id, }; const iconClasses = { @@ -169,55 +165,30 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { 'hide-icon': disableDropdown, }; - const participantIsFollowed = this.following?.id === id; - - const options = Object.values(WIODropdownOptions) - .map((label, index) => ({ - label: participantIsFollowed && index ? 'UNFOLLOW' : label, - id, - name, - color, - slotIndex, - })) - .slice(0, 2); - - const tooltipData: TooltipData = { - name, - }; - - if ( - this.localParticipantJoinedPresence && - joinedPresence && - !this.disablePresenceControls - ) { - tooltipData.action = 'Click to Follow'; - } - const isLastParticipant = index === numberOfParticipants; return html`
    - ${this.getAvatar(participant)} + ${avatar.color}"> + ${this.getAvatar(avatar)}
    ${name} { this.participantColor = participant.color; }); + + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + following.subscribe(); } protected firstUpdated( @@ -148,6 +151,7 @@ export class WhoIsOnlineMessages extends WebComponentsBaseElement { // new messages/interactions be added // The exception is 1 class unique to each message, so the user can target this message in particular private followingMessage() { + console.log('uh', this.following); if (!this.following) return ''; const { name, color } = this.following; diff --git a/src/web-components/who-is-online/components/types.ts b/src/web-components/who-is-online/components/types.ts index f209e7a1..dea427e9 100644 --- a/src/web-components/who-is-online/components/types.ts +++ b/src/web-components/who-is-online/components/types.ts @@ -1,21 +1,3 @@ -export enum WIODropdownOptions { - GOTO = 'go to', - LOCAL_FOLLOW = 'follow', - LOCAL_UNFOLLOW = 'unfollow', - FOLLOW = 'everyone follows me', - UNFOLLOW = 'stop followers', - PRIVATE = 'private mode', - LEAVE_PRIVATE = 'leave private mode', - GATHER = 'gather all', - STOP_GATHER = 'stop gather all', -} - -export interface Following { - id: string; - name: string; - color: string; -} - export interface Options { label: string; id: string; @@ -29,11 +11,6 @@ export interface LocalParticipantData { joinedPresence: boolean; } -export interface TooltipData { - name: string; - action?: string; -} - export enum VerticalSide { TOP = 'top-side', BOTTOM = 'bottom-side', diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index cae9bdd8..61daeabb 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -1,4 +1,5 @@ -import { CSSResultGroup, LitElement, PropertyValueMap, html } from 'lit'; +// @ts-nocheck +import { CSSResultGroup, LitElement, Part, PropertyValueMap, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; @@ -6,12 +7,12 @@ import { repeat } from 'lit/directives/repeat.js'; import { RealtimeEvent } from '../../common/types/events.types'; import { INDEX_IS_WHITE_TEXT } from '../../common/types/meeting-colors.types'; import { StoreType } from '../../common/types/stores.types'; -import { Participant } from '../../components/who-is-online/types'; +import { Avatar, Participant, WIODropdownOptions } from '../../components/who-is-online/types'; +import { Following } from '../../services/stores/who-is-online/types'; import { WebComponentsBase } from '../base'; import importStyle from '../base/utils/importStyle'; import type { LocalParticipantData, TooltipData } from './components/types'; -import { Following, WIODropdownOptions } from './components/types'; import { whoIsOnlineStyle } from './css/index'; const WebComponentsBaseElement = WebComponentsBase(LitElement); @@ -21,23 +22,20 @@ const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, whoIsOnlineSt export class WhoIsOnline extends WebComponentsBaseElement { static styles = styles; declare position: string; - declare participants: Participant[]; declare open: boolean; - declare following: Following | undefined; declare isPrivate: boolean; declare everyoneFollowsMe: boolean; declare showTooltip: boolean; + private following: Following | undefined; private localParticipantData: LocalParticipantData; - private disablePresenceControls: boolean; - + private amountOfExtras: number; private disableDropdown: boolean; + private participants: Participant[]; static properties = { position: { type: String }, - participants: { type: Object }, open: { type: Boolean }, - following: { type: Object }, localParticipantColor: { type: String }, isPrivate: { type: Boolean }, everyoneFollowsMe: { type: Boolean }, @@ -51,6 +49,12 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.open = false; const { localParticipant } = this.useStore(StoreType.GLOBAL); + const { participants, following, extras } = this.useStore(StoreType.WHO_IS_ONLINE); + participants.subscribe(); + following.subscribe(); + extras.subscribe((participants: Participant[]) => { + this.amountOfExtras = participants.length; + }); localParticipant.subscribe((value: Participant) => { const joinedPresence = value.activeComponents?.some((component) => @@ -105,21 +109,8 @@ export class WhoIsOnline extends WebComponentsBaseElement { return 'bottom-left'; } - private renderExcessParticipants() { - const excess = this.participants.length - 4; - if (excess <= 0) return html``; - - const participants = this.participants - .slice(4) - .map(({ name, color, id, slotIndex, isLocal, avatar, joinedPresence }) => ({ - name, - color, - id, - slotIndex, - avatar, - isLocal, - joinedPresence, - })); + private renderExtras() { + if (!this.amountOfExtras) return; const classes = { 'who-is-online__participant': true, @@ -127,16 +118,13 @@ export class WhoIsOnline extends WebComponentsBaseElement { 'excess_participants--open': this.open, }; - const dropdown = html` + console.log('hm?'); + + return html`
    -
    +${excess}
    +
    + +${this.amountOfExtras} +
    `; - - return dropdown; } - private dropdownOptionsHandler = ({ detail }: CustomEvent) => { - const { id, label, name, color, slotIndex } = detail; - - if (label === WIODropdownOptions['GOTO']) { - this.emitEvent(RealtimeEvent.REALTIME_GO_TO_PARTICIPANT, { id }); - } - - if ([WIODropdownOptions.LOCAL_FOLLOW, WIODropdownOptions.LOCAL_UNFOLLOW].includes(label)) { - if (this.following?.id === id) { - this.stopFollowing(); - return; - } - - if (this.everyoneFollowsMe) { - this.stopEveryoneFollowsMe(); - } - - this.following = { name, id, color }; - this.swapParticipantBeingFollowedPosition(); - this.emitEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { id }); - } - - if ([WIODropdownOptions.PRIVATE, WIODropdownOptions.LEAVE_PRIVATE].includes(label)) { - this.isPrivate = label === WIODropdownOptions.PRIVATE; - - if (this.everyoneFollowsMe) { - this.stopEveryoneFollowsMe(); - } - - this.emitEvent(RealtimeEvent.REALTIME_PRIVATE_MODE, { id, isPrivate: this.isPrivate }); - this.everyoneFollowsMe = false; - } - - if ([WIODropdownOptions.FOLLOW, WIODropdownOptions.UNFOLLOW].includes(label)) { - if (this.everyoneFollowsMe) { - this.stopEveryoneFollowsMe(); - return; - } - - if (this.following) { - this.stopFollowing(); - } - - if (this.isPrivate) { - this.cancelPrivate(); - } - - this.everyoneFollowsMe = true; - this.following = undefined; - this.emitEvent(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, { id, name, color, slotIndex }); - } - - if (label === WIODropdownOptions.GATHER) { - this.emitEvent(RealtimeEvent.REALTIME_GATHER, { id }); - } - }; - private toggleShowTooltip = () => { this.showTooltip = !this.showTooltip; }; - private stopEveryoneFollowsMe() { - this.everyoneFollowsMe = false; - this.emitEvent(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, undefined); - } - - private getAvatar(participant: Participant) { - if (participant.avatar?.imageUrl) { + private getAvatar({ color, imageUrl, firstLetter }: Avatar) { + if (imageUrl) { return html` `; } - const letterColor = INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : '#26242A'; + const letterColor = + /* INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : */ '#26242A'; return html`
    - ${participant.name?.at(0).toUpperCase()} + ${firstLetter}
    `; } - private getOptions(participant: Participant, isBeingFollowed: boolean, isLocal: boolean) { - if (this.disablePresenceControls) return []; + private cancelPrivate() { + this.isPrivate = undefined; + this.emitEvent(RealtimeEvent.REALTIME_PRIVATE_MODE, { id: this.localParticipantData.id }); + } - const { id, slotIndex, name, color } = participant; - const baseOption = { id, name, color, slotIndex }; - const { isPrivate } = this; + private renderParticipants() { + if (!this.participants.length) return html``; - const labels = isLocal - ? [ - 'GATHER', - this.everyoneFollowsMe ? 'UNFOLLOW' : 'FOLLOW', - isPrivate ? 'LEAVE_PRIVATE' : 'PRIVATE', - ] - : ['GOTO', isBeingFollowed ? 'LOCAL_UNFOLLOW' : 'LOCAL_FOLLOW']; + return html` + ${repeat( + this.participants, + (participant) => participant.id, + (participant, index) => { + const { avatar, id, name, tooltip, controls, disableDropdown, isLocalParticipant } = + participant; + const position = this.dropdownPosition(index); - const options = labels.map((label) => ({ - ...baseOption, - label: WIODropdownOptions[label], - })); + const participantIsFollowed = this.following?.id === id; + const classList = { + 'who-is-online__participant': true, + 'disable-dropdown': disableDropdown, + followed: participantIsFollowed || (this.everyoneFollowsMe && isLocalParticipant), + private: isLocalParticipant && this.isPrivate, + }; - return options; + return html` + +
    + ${this.getAvatar(avatar)} +
    +
    + `; + }, + )} + `; } - private getIcons(isLocal: boolean, isBeingFollowed: boolean) { - if (this.disablePresenceControls) return []; + // ----- handle presence controls options ----- + private dropdownOptionsHandler = ({ detail: { label, participantId, source } }: CustomEvent) => { + if (label === WIODropdownOptions.GOTO) { + this.handleGoTo(participantId); + } - return isLocal - ? ['gather', this.everyoneFollowsMe ? 'send-off' : 'send', 'eye_inative'] - : ['place', isBeingFollowed ? 'send-off' : 'send']; - } + if (label === WIODropdownOptions.LOCAL_FOLLOW) { + this.handleLocalFollow(participantId, source); + } + + if (label === WIODropdownOptions.LOCAL_UNFOLLOW) { + this.handleLocalUnfollow(); + } + + if (label === WIODropdownOptions.PRIVATE) { + this.handlePrivate(participantId); + } - private putLocalParticipationFirst() { - if (!this.participants[0] || this.participants[0].isLocal) return; + if (label === WIODropdownOptions.LEAVE_PRIVATE) { + this.handleCancelPrivate(participantId); + } - const localParticipant = this.participants?.find(({ isLocal }) => isLocal); - if (!localParticipant) return; + if (label === WIODropdownOptions.FOLLOW) { + this.handleFollow(participantId, source); + } + + if (label === WIODropdownOptions.UNFOLLOW) { + this.handleStopFollow(); + } + + if (label === WIODropdownOptions.GATHER) { + this.handleGatherAll(participantId); + } + }; - const participants = [...this.participants]; - const localParticipantIndex = participants.indexOf(localParticipant); - participants.splice(localParticipantIndex, 1); - participants.unshift(localParticipant); - this.participants = participants; + private handleGoTo(participantId: string) { + this.emitEvent(RealtimeEvent.REALTIME_GO_TO_PARTICIPANT, { id: participantId }); } - private swapParticipantBeingFollowedPosition() { - const a = this.participants?.findIndex(({ id }) => id === this.following?.id); - const b = 1; + private handleLocalFollow(participantId: string, source: string) { + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + const participants = this.useStore(StoreType.WHO_IS_ONLINE)[source].value; - if (a < 4 || !a) return; + const { + id, + name, + avatar: { color }, + } = participants.find(({ id }) => id === participantId) as Participant; - const participants = [...this.participants]; - const temp = participants[a]; - participants[a] = participants[b]; - participants[b] = temp; - this.participants = participants; + if (this.everyoneFollowsMe) { + this.handleStopFollow(); + } + following.publish({ name, id, color }); + this.emitEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { id }); } - private stopFollowing() { - this.following = undefined; + private handleLocalUnfollow() { + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + following.publish(undefined); this.emitEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { id: undefined }); } - private cancelPrivate() { - this.isPrivate = undefined; - this.emitEvent(RealtimeEvent.REALTIME_PRIVATE_MODE, { id: this.localParticipantData.id }); - } + private handlePrivate(id: string) { + if (this.everyoneFollowsMe) { + this.handleStopFollow(); + } - private renderParticipants() { - if (!this.participants) return html``; + this.emitEvent(RealtimeEvent.REALTIME_PRIVATE_MODE, { id, isPrivate: true }); + this.isPrivate = true; + } - this.putLocalParticipationFirst(); - this.swapParticipantBeingFollowedPosition(); + private handleCancelPrivate(id: string) { + this.emitEvent(RealtimeEvent.REALTIME_PRIVATE_MODE, { id, isPrivate: false }); + this.isPrivate = false; + } - return html`
    - ${repeat( - this.participants.slice(0, 4), - (participant) => participant.id, - (participant, index) => { - const { joinedPresence, isLocal, id, name, color } = participant; - const participantIsFollowed = this.following?.id === id; - const options = this.getOptions(participant, participantIsFollowed, isLocal); - const icons = this.getIcons(isLocal, participantIsFollowed); - const position = this.dropdownPosition(index); - const disableDropdown = - !joinedPresence || this.disableDropdown || this.disablePresenceControls; + private handleFollow(participantId: string, source: string) { + if (this.isPrivate) { + this.cancelPrivate(); + } - const classList = { - 'who-is-online__participant': true, - 'disable-dropdown': disableDropdown, - followed: participantIsFollowed || (isLocal && this.everyoneFollowsMe), - private: isLocal && this.isPrivate, - }; + const participants = this.useStore(StoreType.WHO_IS_ONLINE)[source].value; + const { + id, + name, + avatar: { color }, + } = participants.find(({ id }) => id === participantId); - const append = isLocal ? ' (you)' : ''; - const participantName = name + append; + this.everyoneFollowsMe = true; - const tooltipData: TooltipData = { - name, - }; + const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + following.publish(undefined); - const showAction = !this.disablePresenceControls; + this.emitEvent(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, { + id, + name, + color /* , slotIndex */, + }); + } - if ( - showAction && - this.localParticipantData?.joinedPresence && - joinedPresence && - !isLocal - ) { - tooltipData.action = 'Click to Follow'; - } + private handleStopFollow() { + this.everyoneFollowsMe = false; + this.emitEvent(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, undefined); + } - if (isLocal) { - tooltipData.action = 'You'; - } + private handleGatherAll(id: string) { + if (this.isPrivate) { + this.cancelPrivate(); + } - return html` - -
    - ${this.getAvatar(participant)} -
    -
    - `; - }, - )} - ${this.renderExcessParticipants()} -
    `; + this.emitEvent(RealtimeEvent.REALTIME_GATHER, { id }); } updated(changedProperties) { @@ -396,14 +348,15 @@ export class WhoIsOnline extends WebComponentsBaseElement { protected render() { return html`
    - ${this.renderParticipants()} +
    + ${this.renderParticipants()} ${this.renderExtras()} +
    `; } From e16289b36178451b39ecbbb5a3628c3a035fa663 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Fri, 5 Apr 2024 15:53:30 -0300 Subject: [PATCH 71/83] fix: remove mouses that are already in screen when following participant --- src/components/presence-mouse/canvas/index.ts | 2 +- src/web-components/who-is-online/components/messages.ts | 1 - src/web-components/who-is-online/who-is-online.ts | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index b1107010..6717405a 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -262,7 +262,7 @@ export class PointersCanvas extends BaseComponent { private updateParticipantsMouses = (): void => { this.presences.forEach((mouse) => { - if (!mouse?.visible) { + if (!mouse?.visible || (this.following && this.following !== mouse.id)) { this.removePresenceMouseParticipant(mouse.id); return; } diff --git a/src/web-components/who-is-online/components/messages.ts b/src/web-components/who-is-online/components/messages.ts index d93f9407..f54c8d7a 100644 --- a/src/web-components/who-is-online/components/messages.ts +++ b/src/web-components/who-is-online/components/messages.ts @@ -151,7 +151,6 @@ export class WhoIsOnlineMessages extends WebComponentsBaseElement { // new messages/interactions be added // The exception is 1 class unique to each message, so the user can target this message in particular private followingMessage() { - console.log('uh', this.following); if (!this.following) return ''; const { name, color } = this.following; diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index 61daeabb..8fd406dc 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -118,8 +118,6 @@ export class WhoIsOnline extends WebComponentsBaseElement { 'excess_participants--open': this.open, }; - console.log('hm?'); - return html` Date: Fri, 5 Apr 2024 16:53:11 -0300 Subject: [PATCH 72/83] fix: comment ellipsis and close/open in large texts --- .../comments/components/annotation-item.ts | 2 +- .../comments/components/comment-item.test.ts | 35 +++++--- .../comments/components/comment-item.ts | 84 ++++++++++++------- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/web-components/comments/components/annotation-item.ts b/src/web-components/comments/components/annotation-item.ts index f758e21d..a1ac6a1f 100644 --- a/src/web-components/comments/components/annotation-item.ts +++ b/src/web-components/comments/components/annotation-item.ts @@ -131,7 +131,7 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { } } - private selectAnnotation = () => { + private selectAnnotation = (event: PointerEvent): void => { const { uuid } = this.annotation; document.body.dispatchEvent(new CustomEvent('select-annotation', { detail: { uuid } })); }; diff --git a/src/web-components/comments/components/comment-item.test.ts b/src/web-components/comments/components/comment-item.test.ts index 6edd0c37..c6c1cc09 100644 --- a/src/web-components/comments/components/comment-item.test.ts +++ b/src/web-components/comments/components/comment-item.test.ts @@ -50,11 +50,11 @@ describe('CommentsCommentItem', () => { const createdAt = element.shadowRoot!.querySelector( '.comments__comment-item__date', ) as HTMLSpanElement; - expect(createdAt.textContent).toEqual(DateTime.now().toFormat('yyyy-dd-MM')); + expect(createdAt.innerText).toEqual(DateTime.now().toFormat('yyyy-dd-MM')); const text = element.shadowRoot!.querySelector( '.comments__comment-item__content', - ) as HTMLSpanElement; + ) as HTMLParagraphElement; expect(text.innerText).toEqual('This is a comment'); }); @@ -209,7 +209,7 @@ describe('CommentsCommentItem', () => { expect(element.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('delete-annotation')); }); - test('when click in text should expand elipis when text is bigger than 120', async () => { + test('when text is bigger than 120 and annotation is not selected should add line-clamp class to text', async () => { element = await createElement({ ...DEFAULT_ELEMENT_OPTIONS, text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eget aliquam lacinia, nisl nisl aliquet nisl, eget aliquam nisl nisl eget.', @@ -217,27 +217,36 @@ describe('CommentsCommentItem', () => { await element['updateComplete']; - const text = element.shadowRoot!.querySelector('.annotation-content') as HTMLElement; - text.click(); - - await element['updateComplete']; + const paragraph = element.shadowRoot!.getElementById('comment-text') as HTMLParagraphElement; - expect(element['expandElipsis']).toBeTruthy(); + expect(paragraph.classList.contains('line-clamp')).toBeTruthy(); }); - test('when click in text should not expand elipis when text is smaller than 120', async () => { + test('when text is bigger than 120 and annotation is selected should not have line-clamp to text', async () => { element = await createElement({ ...DEFAULT_ELEMENT_OPTIONS, - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eget aliquam lacinia, nisl nisl aliquet nisl, eget aliquam nisl nisl eget.', }); await element['updateComplete']; - const text = element.shadowRoot!.querySelector('.annotation-content') as HTMLElement; - text.click(); + const paragraph = element.shadowRoot!.getElementById('comment-text') as HTMLParagraphElement; + paragraph.click(); + + element['isSelected'] = true; + await element['updateComplete']; + + expect(paragraph.classList.contains('line-clamp')).toBeFalsy(); + }); + + test('should not add line-clamp class when text is smaller than 120', async () => { + element = await createElement({ + ...DEFAULT_ELEMENT_OPTIONS, + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }); await element['updateComplete']; - expect(element['expandElipsis']).toBeFalsy(); + expect(element['line-clamp']).toBeFalsy(); }); }); diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index c4cf2771..1da6250d 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -32,7 +32,7 @@ export class CommentsCommentItem extends WebComponentsBaseElement { declare mode: CommentMode; declare deleteCommentModalOpen: boolean; declare primaryComment: boolean; - declare expandElipsis: boolean; + declare isSelected: boolean; declare annotationFilter: string; declare participantsList: ParticipantByGroupApi[]; declare mentions: ParticipantByGroupApi[]; @@ -51,7 +51,7 @@ export class CommentsCommentItem extends WebComponentsBaseElement { mode: { type: String }, deleteCommentModalOpen: { type: Boolean }, primaryComment: { type: Boolean }, - expandElipsis: { type: Boolean }, + isSelected: { type: Boolean }, annotationFilter: { type: String }, participantsList: { type: Object }, mentions: { type: Array }, @@ -65,8 +65,47 @@ export class CommentsCommentItem extends WebComponentsBaseElement { this.updateComplete.then(() => { importStyle.call(this, ['comments']); }); + + document.body.addEventListener('select-annotation', this.selectAnnotation); + } + + connectedCallback(): void { + super.connectedCallback(); + + window.document.body.addEventListener('select-annotation', this.selectAnnotation); + window.document.body.addEventListener('keyup', this.unselectAnnotationEsc); + window.document.body.addEventListener('unselect-annotation', this.unselectAnnotation); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + + window.document.body.removeEventListener('select-annotation', this.selectAnnotation); + window.document.body.removeEventListener('keyup', this.unselectAnnotationEsc); + window.document.body.removeEventListener('unselect-annotation', this.unselectAnnotation); } + private unselectAnnotationEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.isSelected = false; + } + }; + + private unselectAnnotation = () => { + this.isSelected = false; + }; + + private selectAnnotation = ({ detail }: CustomEvent) => { + const { uuid } = detail; + + if (this.isSelected) { + this.isSelected = false; + return; + } + + this.isSelected = uuid === this.annotationId; + }; + private updateComment = ({ detail }: CustomEvent) => { const { text, mentions } = detail; this.text = text; @@ -159,12 +198,6 @@ export class CommentsCommentItem extends WebComponentsBaseElement { } }; - const expandElipsis = () => { - if (this.text.length < 120) return; - - this.expandElipsis = true; - }; - const textareaHtml = () => { const classes = { 'comments__comment-item--editable': true, @@ -189,20 +222,16 @@ export class CommentsCommentItem extends WebComponentsBaseElement { const commentText = () => { const textClasses = { - editing: this.mode === CommentMode.EDITABLE, - 'annotation-content': true, text: true, 'text-big': true, 'sv-gray-700': true, + 'annotation-content': true, [this.getClasses('content')]: true, - 'line-clamp': !this.expandElipsis && this.text.length > 120, + editing: this.mode === CommentMode.EDITABLE, + 'line-clamp': !this.isSelected && this.text.length > 120, }; - return html` - ${this.text} - `; + return html`

    ${this.text}

    `; }; const closeModal = () => { @@ -225,12 +254,12 @@ export class CommentsCommentItem extends WebComponentsBaseElement {
    ${this.getAvatar()} - ${this.username} - ${humanizeDate(this.createdAt)} + + ${this.username} + + + ${humanizeDate(this.createdAt)} +
    event.stopPropagation()} + @click=${(event: Event) => { + event.stopPropagation(); + }} classesPrefix="comments__dropdown" parentComponent="comments" > From f912d0aedd2067d9c556f26d066b528b8e334882 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sun, 7 Apr 2024 22:40:59 -0300 Subject: [PATCH 73/83] fix: go to participant being followed right away, instead of waiting for the to move --- src/components/presence-mouse/canvas/index.ts | 1 + src/components/presence-mouse/html/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 6717405a..12963c50 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -367,6 +367,7 @@ export class PointersCanvas extends BaseComponent { private followMouse = (id: string) => { this.following = id; + this.goToMouse(id); }; /** diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 3e3cfe8e..471483f0 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -223,6 +223,7 @@ export class PointersHTML extends BaseComponent { */ private followMouse = (id: string) => { this.userBeingFollowedId = id; + this.goToMouse(id); }; /** From ee83eb1afd53c5d97c3d9be4e2d2ef56e15e2dd4 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sun, 7 Apr 2024 22:41:31 -0300 Subject: [PATCH 74/83] fix: don't hide participant mouse if they're still in the container area --- src/components/presence-mouse/html/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index 471483f0..4a17da03 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -190,7 +190,9 @@ export class PointersHTML extends BaseComponent { * @function onMyParticipantMouseLeave * @returns {void} */ - private onMyParticipantMouseLeave = (): void => { + private onMyParticipantMouseLeave = (event: MouseEvent): void => { + const { x, y, width, height } = this.container.getBoundingClientRect(); + if (event.x > 0 && event.y > 0 && event.x < x + width && event.y < y + height) return; this.room.presence.update({ visible: false }); }; From 3e9f02a14fa2877cb996b88d79227df529c83a98 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sun, 7 Apr 2024 22:52:27 -0300 Subject: [PATCH 75/83] feat: update superviz-dropdown options and return --- src/web-components/comments/comments.ts | 6 ++- .../comments/components/annotation-filter.ts | 39 +++++++++---------- .../comments/components/comment-item.ts | 8 ++-- .../comments/components/types.ts | 4 +- src/web-components/dropdown/index.ts | 19 +++------ src/web-components/dropdown/types.ts | 4 +- 6 files changed, 35 insertions(+), 45 deletions(-) diff --git a/src/web-components/comments/comments.ts b/src/web-components/comments/comments.ts index b30064a5..d5835194 100644 --- a/src/web-components/comments/comments.ts +++ b/src/web-components/comments/comments.ts @@ -84,8 +84,10 @@ export class Comments extends WebComponentsBaseElement { } private setFilter({ detail }) { - const { filter } = detail; - this.annotationFilter = filter; + const { + filter: { label }, + } = detail; + this.annotationFilter = label; } private getOffset(offset: number) { diff --git a/src/web-components/comments/components/annotation-filter.ts b/src/web-components/comments/components/annotation-filter.ts index 89a2232e..861afafc 100644 --- a/src/web-components/comments/components/annotation-filter.ts +++ b/src/web-components/comments/components/annotation-filter.ts @@ -4,6 +4,7 @@ import { classMap } from 'lit/directives/class-map.js'; import { WebComponentsBase } from '../../base'; import importStyle from '../../base/utils/importStyle'; +import { DropdownOption } from '../../dropdown/types'; import { annotationFilterStyle } from '../css'; import { AnnotationFilter } from './types'; @@ -11,14 +12,12 @@ import { AnnotationFilter } from './types'; const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, annotationFilterStyle]; -const options = [ +const options: DropdownOption[] = [ { - label: 'All comments', - code: AnnotationFilter.ALL, + label: AnnotationFilter.ALL, }, { - label: 'Resolved comments', - code: AnnotationFilter.RESOLVED, + label: AnnotationFilter.RESOLVED, }, ]; @@ -48,20 +47,21 @@ export class CommentsAnnotationFilter extends WebComponentsBaseElement { }); } + private selectClick = () => { + this.caret = this.caret === 'down' ? 'up' : 'down'; + }; + + private dropdownOptionsHandler = ({ detail }: CustomEvent) => { + this.emitEvent('select', { filter: detail }); + this.selectClick(); + }; + protected render() { const selectedLabel = this.filter === AnnotationFilter.ALL ? options[0].label : options[1].label; - const dropdownOptionsHandler = ({ detail }: CustomEvent) => { - this.emitEvent('select', { filter: detail }); - selectClick(); - }; - - const selectClick = () => { - this.caret = this.caret === 'down' ? 'up' : 'down'; - }; - - const active = this.filter === AnnotationFilter.ALL ? options[0].code : options[1].code; + options[0].active = this.filter === AnnotationFilter.ALL; + options[1].active = this.filter === AnnotationFilter.RESOLVED; const textClasses = { text: true, @@ -77,14 +77,11 @@ export class CommentsAnnotationFilter extends WebComponentsBaseElement {
    diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index 5a7f58f7..d50d8371 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -148,13 +148,13 @@ export class CommentsCommentItem extends WebComponentsBaseElement { }, ]; - const dropdownOptionsHandler = ({ detail }: CustomEvent) => { - if (detail === CommentDropdownOptions.EDIT) { + const dropdownOptionsHandler = ({ detail: { label } }: CustomEvent) => { + if (label === CommentDropdownOptions.EDIT) { this.mode = CommentMode.EDITABLE; this.emitEvent('edit-comment', { editing: true }); } - if (detail === CommentDropdownOptions.DELETE) { + if (label === CommentDropdownOptions.DELETE) { this.deleteCommentModalOpen = true; } }; @@ -247,8 +247,6 @@ export class CommentsCommentItem extends WebComponentsBaseElement { event.stopPropagation()} diff --git a/src/web-components/comments/components/types.ts b/src/web-components/comments/components/types.ts index 5e22b741..8fbd640e 100644 --- a/src/web-components/comments/components/types.ts +++ b/src/web-components/comments/components/types.ts @@ -19,8 +19,8 @@ export enum PinMode { } export enum AnnotationFilter { - ALL = 'all', - RESOLVED = 'resolved', + ALL = 'All comments', + RESOLVED = 'Resolved comments', } export type HorizontalSide = 'left' | 'right'; diff --git a/src/web-components/dropdown/index.ts b/src/web-components/dropdown/index.ts index 7057dffb..4189b1d1 100644 --- a/src/web-components/dropdown/index.ts +++ b/src/web-components/dropdown/index.ts @@ -21,8 +21,6 @@ export class Dropdown extends WebComponentsBaseElement { declare options: DropdownOption[]; declare returnData: { [k: string]: any }; - declare returnTo: string; - declare active: string | object; declare icons?: string[]; declare name?: string; declare tooltipData: { name: string; action: string }; @@ -46,8 +44,6 @@ export class Dropdown extends WebComponentsBaseElement { disabled: { type: Boolean }, align: { type: String }, options: { type: Array }, - returnTo: { type: String }, - active: { type: [String, Object] }, icons: { type: Array }, name: { type: String }, tooltipData: { type: Object }, @@ -136,12 +132,12 @@ export class Dropdown extends WebComponentsBaseElement { }); }; - private callbackSelected = ({ option }: DropdownOption) => { + private callbackSelected = ({ label }: DropdownOption) => { this.open = false; this.emitEvent( 'selected', - { ...this.returnData, label: option }, + { ...this.returnData, label }, { bubbles: false, composed: true, @@ -244,19 +240,16 @@ export class Dropdown extends WebComponentsBaseElement { } private get listOptions() { - return this.options.map(({ option, icon, data }) => { + return this.options.map(({ label, icon, active }) => { const liClasses = { text: true, [this.getClass('item')]: true, 'text-bold': true, - active: option?.[this.returnTo] && this.active === option?.[this.returnTo], + active, }; - return html`
  • this.callbackSelected({ option, data })} - class=${classMap(liClasses)} - > - ${this.getIcon(icon)} ${this.getLabel(option)} + return html`
  • this.callbackSelected({ label })} class=${classMap(liClasses)}> + ${this.getIcon(icon)} ${this.getLabel(label)}
  • `; }); } diff --git a/src/web-components/dropdown/types.ts b/src/web-components/dropdown/types.ts index 46b8df98..8bcfcec0 100644 --- a/src/web-components/dropdown/types.ts +++ b/src/web-components/dropdown/types.ts @@ -1,5 +1,5 @@ export interface DropdownOption { - option: string; + label: string; icon?: string; - data?: any; + active?: boolean; } From 794760b6e7ca61c9df09c6e2e449c0b248a488ee Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Sun, 7 Apr 2024 23:06:11 -0300 Subject: [PATCH 76/83] feat: final wio refactor adjustments --- src/components/who-is-online/index.ts | 180 ++++++++++++------ src/components/who-is-online/types.ts | 10 +- .../who-is-online/components/dropdown.ts | 5 +- .../who-is-online/who-is-online.ts | 72 ++++--- 4 files changed, 160 insertions(+), 107 deletions(-) diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index 6e89e473..a4fd4e3d 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -50,7 +50,7 @@ export class WhoIsOnline extends BaseComponent { disablePrivateMode, disableGatherAll, disableFollowMe, - extras, + following, } = this.useStore(StoreType.WHO_IS_ONLINE); disablePresenceControls.publish(options.flags?.disablePresenceControls); @@ -59,6 +59,8 @@ export class WhoIsOnline extends BaseComponent { disablePrivateMode.publish(options.flags?.disablePrivateMode); disableGatherAll.publish(options.flags?.disableGatherAll); disableFollowMe.publish(options.flags?.disableFollowMe); + + following.subscribe(); } /** @@ -239,8 +241,6 @@ export class WhoIsOnline extends BaseComponent { private followMousePointer = ({ detail }: CustomEvent) => { this.eventBus.publish(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, detail.id); - const { participants, following } = this.useStore(StoreType.WHO_IS_ONLINE); - if (this.following) { this.publish(WhoIsOnlineEvent.START_FOLLOWING_PARTICIPANT, this.following); } @@ -249,21 +249,11 @@ export class WhoIsOnline extends BaseComponent { this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); } - participants.publish( - participants.value.map((participant: Participant) => { - const participantId = participant.id; - const disableDropdown = this.shouldDisableDropdown({ - activeComponents: participant.activeComponents, - }); - const presenceEnabled = !disableDropdown; - const controls = this.getControls({ participantId, presenceEnabled }) ?? []; + if (detail.source === 'extras') { + this.highlightParticipantBeingFollowed(); + } - return { - ...participant, - controls, - }; - }), - ); + this.updateParticipantsControls(detail.id); }; /** @@ -288,25 +278,21 @@ export class WhoIsOnline extends BaseComponent { } }; - private setFollow = (followingData) => { + private setFollow = (followingData: AblyParticipant) => { if (followingData.clientId === this.localParticipantId) return; - this.followMousePointer({ detail: { id: followingData?.data?.id } } as CustomEvent); + const data = followingData.data?.id ? followingData.data : undefined; const { following } = this.useStore(StoreType.WHO_IS_ONLINE); + following.publish(data); - if (!followingData.data.id) { - following.publish(undefined); - return; - } - - following.publish(followingData.data); + this.followMousePointer({ detail: { id: data?.id } } as CustomEvent); }; - private follow = (data: CustomEvent) => { - const { everyoneFollowsMe, participants } = this.useStore(StoreType.WHO_IS_ONLINE); - everyoneFollowsMe.publish(!!data.detail?.id); + private follow = ({ detail }: CustomEvent) => { + const { everyoneFollowsMe } = this.useStore(StoreType.WHO_IS_ONLINE); + everyoneFollowsMe.publish(!!detail?.id); - this.realtime.setFollowWIOParticipant({ ...data.detail }); + this.realtime.setFollowWIOParticipant({ ...detail }); if (this.following) { this.publish(WhoIsOnlineEvent.START_FOLLOW_ME, this.following); @@ -316,26 +302,19 @@ export class WhoIsOnline extends BaseComponent { this.publish(WhoIsOnlineEvent.STOP_FOLLOW_ME); } - participants.publish( - participants.value.map((participant) => { - if (participant.id !== this.localParticipantId) return participant; - - const controls = this.getLocalParticipantControls(); - return { - ...participant, - controls, - }; - }), - ); + this.updateParticipantsControls(detail?.id); }; - private stopFollowing = (participant: { clientId: string }) => { + private stopFollowing = (participant: { clientId: string }, stopEvent?: boolean) => { if (participant.clientId !== this.following?.id) return; const { following } = this.useStore(StoreType.WHO_IS_ONLINE); following.publish(undefined); this.eventBus.publish(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, undefined); + + if (stopEvent) return; + this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); }; @@ -346,7 +325,10 @@ export class WhoIsOnline extends BaseComponent { private filterParticipants(participants: AblyParticipant[]) { return participants.filter(({ data: { activeComponents, id, isPrivate } }) => { - if (isPrivate && this.localParticipantId !== id) return false; + if (isPrivate && this.localParticipantId !== id) { + this.stopFollowing(id, true); + return false; + } const isLocal = id === this.localParticipantId; @@ -365,13 +347,13 @@ export class WhoIsOnline extends BaseComponent { private getParticipant(participant: AblyParticipant): Participant { const { avatar: avatarLinks, activeComponents, participantId, name } = participant.data; const isLocalParticipant = participant.clientId === this.localParticipantId; - - const { color } = this.realtime.getSlotColor(participant.data.slotIndex); - const disableDropdown = this.shouldDisableDropdown({ activeComponents }); + const { slotIndex } = participant.data; + const { color } = this.realtime.getSlotColor(slotIndex); + const disableDropdown = this.shouldDisableDropdown({ activeComponents, participantId }); const presenceEnabled = !disableDropdown; const tooltip = this.getTooltipData({ isLocalParticipant, name, presenceEnabled }); - const avatar = this.getAvatar({ avatar: avatarLinks, color, name }); + const avatar = this.getAvatar({ avatar: avatarLinks, color, name, slotIndex }); const controls = this.getControls({ participantId, presenceEnabled }) ?? []; return { @@ -386,9 +368,35 @@ export class WhoIsOnline extends BaseComponent { }; } - private shouldDisableDropdown({ activeComponents }: { activeComponents: string[] | undefined }) { - const { joinedPresence, disablePresenceControls } = this.useStore(StoreType.WHO_IS_ONLINE); - if (joinedPresence.value === false || disablePresenceControls.value === true) return true; + private shouldDisableDropdown({ + activeComponents, + participantId, + }: { + activeComponents: string[] | undefined; + participantId: string; + }) { + const { + joinedPresence, + disablePresenceControls: { value: disablePresenceControls }, + disableFollowMe: { value: disableFollowMe }, + disableFollowParticipant: { value: disableFollowParticipant }, + disableGoToParticipant: { value: disableGoToParticipant }, + disableGatherAll: { value: disableGatherAll }, + disablePrivateMode: { value: disablePrivateMode }, + } = this.useStore(StoreType.WHO_IS_ONLINE); + + if ( + joinedPresence.value === false || + disablePresenceControls.value === true || + (participantId === this.localParticipantId && + disableFollowMe && + disablePrivateMode && + disableGatherAll) || + (participantId !== this.localParticipantId && + disableFollowParticipant && + disableGoToParticipant) + ) + return true; return !activeComponents?.some((component) => component.toLowerCase().includes('presence')); } @@ -415,11 +423,21 @@ export class WhoIsOnline extends BaseComponent { return data; } - private getAvatar({ avatar, color, name }: { avatar: Avatar; name: string; color: string }) { + private getAvatar({ + avatar, + color, + name, + slotIndex, + }: { + avatar: Avatar; + name: string; + color: string; + slotIndex: number; + }) { const imageUrl = avatar?.imageUrl; const firstLetter = name?.at(0).toUpperCase() ?? 'A'; - return { imageUrl, firstLetter, color }; + return { imageUrl, firstLetter, color, slotIndex }; } private getControls({ @@ -448,16 +466,16 @@ export class WhoIsOnline extends BaseComponent { const controls: DropdownOption[] = []; if (!disableGoToParticipant.value) { - controls.push({ option: WIODropdownOptions['GOTO'], icon: 'place' }); + controls.push({ label: WIODropdownOptions['GOTO'], icon: 'place' }); } if (!disableFollowParticipant.value) { const isBeingFollowed = following.value?.id === participantId; - const option = isBeingFollowed + const label = isBeingFollowed ? WIODropdownOptions['LOCAL_UNFOLLOW'] : WIODropdownOptions['LOCAL_FOLLOW']; const icon = isBeingFollowed ? 'send-off' : 'send'; - controls.push({ option, icon }); + controls.push({ label, icon }); } return controls; @@ -475,25 +493,25 @@ export class WhoIsOnline extends BaseComponent { const controls: DropdownOption[] = []; if (!disableGatherAll) { - controls.push({ option: WIODropdownOptions['GATHER'], icon: 'gather' }); + controls.push({ label: WIODropdownOptions['GATHER'], icon: 'gather' }); } if (!disableFollowMe) { const icon = everyoneFollowsMe ? 'send-off' : 'send'; - const option = everyoneFollowsMe + const label = everyoneFollowsMe ? WIODropdownOptions['UNFOLLOW'] : WIODropdownOptions['FOLLOW']; - controls.push({ option, icon }); + controls.push({ label, icon }); } if (!disablePrivateMode) { const icon = privateMode ? 'eye_inative' : 'eye'; - const option = privateMode + const label = privateMode ? WIODropdownOptions['LEAVE_PRIVATE'] : WIODropdownOptions['PRIVATE']; - controls.push({ option, icon }); + controls.push({ label, icon }); } return controls; @@ -516,4 +534,48 @@ export class WhoIsOnline extends BaseComponent { const { extras } = this.useStore(StoreType.WHO_IS_ONLINE); extras.publish(participantsList); }; + + private updateParticipantsControls(participantId: string | undefined) { + const { participants } = this.useStore(StoreType.WHO_IS_ONLINE); + + participants.publish( + participants.value.map((participant: Participant) => { + if (participantId && participant.id !== participantId) return participant; + + const { id } = participant; + const disableDropdown = this.shouldDisableDropdown({ + activeComponents: participant.activeComponents, + participantId: id, + }); + const presenceEnabled = !disableDropdown; + const controls = this.getControls({ participantId: id, presenceEnabled }) ?? []; + + return { + ...participant, + controls, + }; + }), + ); + } + + private highlightParticipantBeingFollowed() { + const { + extras, + participants, + following: { value: following }, + } = this.useStore(StoreType.WHO_IS_ONLINE); + + const firstParticipant = participants.value[0]; + + const participantId = extras.value.findIndex((participant) => participant.id === following.id); + const participant = extras.value.splice(participantId, 1)[0]; + + participants.value.unshift(firstParticipant); + participants.value[1] = participant; + const lastParticipant = participants.value.pop(); + + extras.value.push(lastParticipant); + extras.publish(extras.value); + participants.publish(participants.value); + } } diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 06d1926c..21dfe3ae 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -15,7 +15,8 @@ export interface TooltipData { export interface Avatar { imageUrl: string; - firstLetter?: string; + firstLetter: string; + slotIndex: number; color: string; } @@ -28,15 +29,8 @@ export interface Participant { avatar: Avatar; activeComponents: string[]; isLocalParticipant: boolean; - // beingFollowed?: boolean; - // slotIndex?: number; - // make go immediately to the mouse when following } -// refactor everywhere that uses superviz-dropdown because of icon changes -// and label removed too -// create tooltip inside WIO store - export type WhoIsOnlinePosition = Position | `${Position}` | string | ''; export interface WhoIsOnlineOptions { diff --git a/src/web-components/who-is-online/components/dropdown.ts b/src/web-components/who-is-online/components/dropdown.ts index 264cfe78..74cecc26 100644 --- a/src/web-components/who-is-online/components/dropdown.ts +++ b/src/web-components/who-is-online/components/dropdown.ts @@ -115,7 +115,7 @@ export class WhoIsOnlineDropdown extends WebComponentsBaseElement { }; }; - private getAvatar({ color, imageUrl, firstLetter }: Avatar) { + private getAvatar({ color, imageUrl, firstLetter, slotIndex }: Avatar) { if (imageUrl) { return html` `; } - const letterColor = - /* INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : */ '#26242A'; + const letterColor = INDEX_IS_WHITE_TEXT.includes(slotIndex) ? '#FFFFFF' : '#26242A'; return html`
    `; } - const letterColor = - /* INDEX_IS_WHITE_TEXT.includes(participant.slotIndex) ? '#FFFFFF' : */ '#26242A'; + const letterColor = INDEX_IS_WHITE_TEXT.includes(slotIndex) ? '#FFFFFF' : '#26242A'; return html`
    { - if (label === WIODropdownOptions.GOTO) { - this.handleGoTo(participantId); - } - - if (label === WIODropdownOptions.LOCAL_FOLLOW) { - this.handleLocalFollow(participantId, source); - } - - if (label === WIODropdownOptions.LOCAL_UNFOLLOW) { - this.handleLocalUnfollow(); - } - - if (label === WIODropdownOptions.PRIVATE) { - this.handlePrivate(participantId); - } - - if (label === WIODropdownOptions.LEAVE_PRIVATE) { - this.handleCancelPrivate(participantId); - } - - if (label === WIODropdownOptions.FOLLOW) { - this.handleFollow(participantId, source); - } - - if (label === WIODropdownOptions.UNFOLLOW) { - this.handleStopFollow(); - } - - if (label === WIODropdownOptions.GATHER) { - this.handleGatherAll(participantId); + switch (label) { + case WIODropdownOptions.GOTO: + this.handleGoTo(participantId); + break; + case WIODropdownOptions.LOCAL_FOLLOW: + this.handleLocalFollow(participantId, source); + break; + case WIODropdownOptions.LOCAL_UNFOLLOW: + this.handleLocalUnfollow(); + break; + case WIODropdownOptions.PRIVATE: + this.handlePrivate(participantId); + break; + case WIODropdownOptions.LEAVE_PRIVATE: + this.handleCancelPrivate(participantId); + break; + case WIODropdownOptions.FOLLOW: + this.handleFollow(participantId, source); + break; + case WIODropdownOptions.UNFOLLOW: + this.handleStopFollow(); + break; + case WIODropdownOptions.GATHER: + this.handleGatherAll(participantId); + break; + default: + break; } }; @@ -271,7 +267,7 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.handleStopFollow(); } following.publish({ name, id, color }); - this.emitEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { id }); + this.emitEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { id, source }); } private handleLocalUnfollow() { @@ -299,11 +295,12 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.cancelPrivate(); } - const participants = this.useStore(StoreType.WHO_IS_ONLINE)[source].value; + const participants: Participant[] = this.useStore(StoreType.WHO_IS_ONLINE)[source].value; + const { id, name, - avatar: { color }, + avatar: { color, slotIndex }, } = participants.find(({ id }) => id === participantId); this.everyoneFollowsMe = true; @@ -314,7 +311,8 @@ export class WhoIsOnline extends WebComponentsBaseElement { this.emitEvent(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, { id, name, - color /* , slotIndex */, + color, + slotIndex, }); } From 0bd01c4ffd3bef4a3ce3e2e8072ab02817bef153 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Apr 2024 14:14:06 -0300 Subject: [PATCH 77/83] feat: create tests and jsdocs for methods --- .../presence-mouse/html/index.test.ts | 29 +- src/components/who-is-online/index.test.ts | 696 ++++++++++++++++-- src/components/who-is-online/index.ts | 136 +++- src/components/who-is-online/types.ts | 1 - src/web-components/comments/comments.test.ts | 6 +- .../comments/components/comment-item.test.ts | 16 +- src/web-components/dropdown/index.test.ts | 72 +- src/web-components/tooltip/index.test.ts | 4 +- .../who-is-online/components/dropdown.test.ts | 213 ++++-- .../who-is-online/who-is-online.test.ts | 228 +++--- .../who-is-online/who-is-online.ts | 4 - 11 files changed, 1100 insertions(+), 305 deletions(-) diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index 7a09f9ad..f4921407 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -449,15 +449,40 @@ describe('MousePointers on HTML', () => { }); describe('onMyParticipantMouseLeave', () => { - test('should call room.presence.update', () => { + test('should only call room.presence.update if mouse is out of container boundaries', () => { const updatePresenceMouseSpy = jest.spyOn( presenceMouseComponent['room']['presence'], 'update', ); - presenceMouseComponent['onMyParticipantMouseLeave'](); + presenceMouseComponent['container'].getBoundingClientRect = jest.fn( + () => + ({ + x: 50, + y: 50, + width: 100, + height: 100, + } as any), + ); + + const mouseEvent1 = { + x: 20, + y: 30, + } as any; + + presenceMouseComponent['onMyParticipantMouseLeave'](mouseEvent1); expect(updatePresenceMouseSpy).toHaveBeenCalledWith({ visible: false }); + updatePresenceMouseSpy.mockClear(); + + const mouseEvent2 = { + x: 75, + y: 75, + } as any; + + presenceMouseComponent['onMyParticipantMouseLeave'](mouseEvent2); + + expect(updatePresenceMouseSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/components/who-is-online/index.test.ts b/src/components/who-is-online/index.test.ts index dff131ed..13f20c4c 100644 --- a/src/components/who-is-online/index.test.ts +++ b/src/components/who-is-online/index.test.ts @@ -9,8 +9,12 @@ import { import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.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'; +import { Following } from '../../services/stores/who-is-online/types'; + +import { Avatar, Participant, TooltipData } from './types'; import { WhoIsOnline } from './index'; @@ -30,7 +34,6 @@ describe('Who Is Online', () => { }); whoIsOnlineComponent['localParticipantId'] = MOCK_LOCAL_PARTICIPANT.id; - whoIsOnlineComponent['element'].updateParticipants = jest.fn(); const gray = MeetingColorsHex[16]; whoIsOnlineComponent['color'] = gray; @@ -97,12 +100,18 @@ describe('Who Is Online', () => { }); describe('onParticipantListUpdate', () => { + let participants; + + beforeEach(() => { + participants = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE)['participants']; + }); + test('should correctly update participant list', () => { whoIsOnlineComponent['onParticipantListUpdate']({ 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, }); - expect(whoIsOnlineComponent['participants'].length).toBe(1); + expect(participants.value.length).toBe(1); whoIsOnlineComponent['onParticipantListUpdate']({ 'unit-test-participant2-ably-id': { @@ -113,7 +122,7 @@ describe('Who Is Online', () => { 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, }); - expect(whoIsOnlineComponent['participants'].length).toBe(2); + expect(participants.value.length).toBe(2); }); test('should not update participant list if participant is does not have whoIsOnline activated', () => { @@ -124,7 +133,7 @@ describe('Who Is Online', () => { }, }); - expect(whoIsOnlineComponent['participants'].length).toBe(0); + expect(participants.value.length).toBe(0); }); test('should not add the same participant twice', () => { @@ -132,13 +141,13 @@ describe('Who Is Online', () => { 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, }); - expect(whoIsOnlineComponent['participants'].length).toBe(1); + expect(participants.value.length).toBe(1); whoIsOnlineComponent['onParticipantListUpdate']({ 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, }); - expect(whoIsOnlineComponent['participants'].length).toBe(1); + expect(participants.value.length).toBe(1); }); test('should not display private participants', () => { @@ -146,7 +155,7 @@ describe('Who Is Online', () => { 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, }); - expect(whoIsOnlineComponent['participants'].length).toBe(1); + expect(participants.value.length).toBe(1); const privateParticipant = { ...MOCK_ABLY_PARTICIPANT, @@ -160,7 +169,7 @@ describe('Who Is Online', () => { 'unit-test-participant1-ably-id': privateParticipant, }); - expect(whoIsOnlineComponent['participants'].length).toBe(0); + expect(participants.value.length).toBe(0); }); test('should display private local participant', () => { @@ -176,30 +185,13 @@ describe('Who Is Online', () => { whoIsOnlineComponent['onParticipantListUpdate'](participantsData); - expect(whoIsOnlineComponent['participants'].length).toBe(1); + expect(participants.value.length).toBe(1); participantsData[MOCK_LOCAL_PARTICIPANT.id].data.isPrivate = true; whoIsOnlineComponent['onParticipantListUpdate'](participantsData); - expect(whoIsOnlineComponent['participants'].length).toBe(1); - }); - - test('should call stopFollowing if previously followed participant leaves', () => { - whoIsOnlineComponent['element'].following = MOCK_ABLY_PARTICIPANT_DATA_1; - whoIsOnlineComponent['following'] = MOCK_ABLY_PARTICIPANT_DATA_2.id; - - whoIsOnlineComponent['stopFollowing'] = jest - .fn() - .mockImplementation(whoIsOnlineComponent['stopFollowing']); - - whoIsOnlineComponent['onParticipantListUpdate']({ - 'unit-test-participant1-ably-id': MOCK_ABLY_PARTICIPANT, - }); - - expect(whoIsOnlineComponent['stopFollowing']).toHaveBeenCalledWith({ - clientId: MOCK_ABLY_PARTICIPANT_DATA_2.id, - }); + expect(participants.value.length).toBe(1); }); }); @@ -273,21 +265,36 @@ describe('Who Is Online', () => { }); test('should set element.data to following.data', () => { + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + whoIsOnlineComponent['setFollow']({ ...MOCK_ABLY_PARTICIPANT, clientId: 'ably-id' }); - expect(whoIsOnlineComponent['element'].following).toBe(MOCK_ABLY_PARTICIPANT_DATA_1); + expect(following.value).toBe(MOCK_ABLY_PARTICIPANT_DATA_1); }); test('should early return if following the local participant', () => { - whoIsOnlineComponent['element'].following = MOCK_ABLY_PARTICIPANT_DATA_2; + const followingData: Following = { + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }; + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish(followingData); whoIsOnlineComponent['setFollow'](MOCK_ABLY_PARTICIPANT); expect(whoIsOnlineComponent['followMousePointer']).not.toHaveBeenCalled(); - expect(whoIsOnlineComponent['element'].following).toBe(MOCK_ABLY_PARTICIPANT_DATA_2); + expect(following.value).toBe(followingData); }); - test('should set element.following to undefiend return if no id is passed', () => { - whoIsOnlineComponent['element'].following = MOCK_ABLY_PARTICIPANT_DATA_2; + test('should set following to undefined if no id is passed', () => { + const followingData: Following = { + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }; + + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish(followingData); whoIsOnlineComponent['setFollow']({ ...MOCK_ABLY_PARTICIPANT, @@ -296,11 +303,11 @@ describe('Who Is Online', () => { }); const event = { - detail: { id: '' }, + detail: { id: undefined }, }; expect(whoIsOnlineComponent['followMousePointer']).toHaveBeenCalledWith(event); - expect(whoIsOnlineComponent['element'].following).toBe(undefined); + expect(following.value).toBe(undefined); }); }); @@ -322,25 +329,32 @@ describe('Who Is Online', () => { describe('stopFollowing', () => { test('should do nothing if participant leaving is not being followed', () => { - whoIsOnlineComponent['element'].following = MOCK_ABLY_PARTICIPANT_DATA_2; - whoIsOnlineComponent['following'] = 'ably-id'; + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish({ + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }); whoIsOnlineComponent['stopFollowing'](MOCK_ABLY_PARTICIPANT); - expect(whoIsOnlineComponent['element'].following).toBe(MOCK_ABLY_PARTICIPANT_DATA_2); - expect(whoIsOnlineComponent['following']).toBe('ably-id'); + expect(following.value).toBeDefined(); + expect(following.value.id).toBe(MOCK_ABLY_PARTICIPANT_DATA_2.id); }); - test('should set element.following to undefined if following the participant who is leaving', () => { - whoIsOnlineComponent['element'].following = MOCK_ABLY_PARTICIPANT_DATA_1; - whoIsOnlineComponent['following'] = MOCK_ABLY_PARTICIPANT_DATA_1.id; + test('should set following to undefined if following the participant who is leaving', () => { + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish({ + color: MOCK_ABLY_PARTICIPANT_DATA_1.color, + id: MOCK_ABLY_PARTICIPANT_DATA_1.id, + name: MOCK_ABLY_PARTICIPANT_DATA_1.name, + }); whoIsOnlineComponent['stopFollowing']({ - ...MOCK_ABLY_PARTICIPANT, clientId: MOCK_ABLY_PARTICIPANT_DATA_1.id, }); - expect(whoIsOnlineComponent['element'].following).toBe(undefined); + expect(following.value).toBe(undefined); expect(whoIsOnlineComponent['following']).toBe(undefined); }); }); @@ -386,17 +400,33 @@ describe('Who Is Online', () => { }); test('should publish "follow" event when followMousePointer is called', () => { + const followingData: Following = { + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }; + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish(followingData); + whoIsOnlineComponent['followMousePointer']({ detail: { id: 'unit-test-id' }, } as CustomEvent); expect(whoIsOnlineComponent['publish']).toHaveBeenCalledWith( WhoIsOnlineEvent.START_FOLLOWING_PARTICIPANT, - 'unit-test-id', + followingData, ); }); test('should publish "stop following" if follow is called with undefined id', () => { + const followingData: Following = { + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }; + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish(undefined); + whoIsOnlineComponent['followMousePointer']({ detail: { id: undefined }, } as CustomEvent); @@ -407,14 +437,15 @@ describe('Who Is Online', () => { }); test('should publish "stop following" event when stopFollowing is called', () => { - whoIsOnlineComponent['element'].following = { - id: 'unit-test-id', - color: 'unit-test-color', - name: 'unit-test-name', - }; + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish({ + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }); whoIsOnlineComponent['stopFollowing']({ - clientId: 'unit-test-id', + clientId: MOCK_ABLY_PARTICIPANT_DATA_2.id, }); expect(whoIsOnlineComponent['publish']).toHaveBeenCalledWith( @@ -443,17 +474,29 @@ describe('Who Is Online', () => { }); test('should publish "follow me" event when follow is called with defined id', () => { + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + const followingData: Following = { + color: MOCK_ABLY_PARTICIPANT_DATA_2.color, + id: MOCK_ABLY_PARTICIPANT_DATA_2.id, + name: MOCK_ABLY_PARTICIPANT_DATA_2.name, + }; + + following.publish(followingData); + whoIsOnlineComponent['follow']({ detail: { id: 'unit-test-id' }, } as CustomEvent); expect(whoIsOnlineComponent['publish']).toHaveBeenCalledWith( WhoIsOnlineEvent.START_FOLLOW_ME, - 'unit-test-id', + followingData, ); }); test('should publish "stop follow me" event when follow is called with undefined id', () => { + const { following } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + following.publish(undefined); + whoIsOnlineComponent['follow']({ detail: { id: undefined }, } as CustomEvent); @@ -480,4 +523,555 @@ describe('Who Is Online', () => { expect(styleElement).toBeFalsy(); }); }); + + describe('followMousePointer', () => { + test('should highlight participant being followed if they are an extra', () => { + whoIsOnlineComponent['highlightParticipantBeingFollowed'] = jest.fn(); + + whoIsOnlineComponent['followMousePointer']({ + detail: { id: 'test-id', source: 'extras' }, + } as any); + + expect(whoIsOnlineComponent['highlightParticipantBeingFollowed']).toHaveBeenCalled(); + }); + }); + + describe('shouldDisableDropdown', () => { + test('should disable dropdown when joinedPresence is false', () => { + whoIsOnlineComponent['useStore'] = jest.fn().mockReturnValue({ + joinedPresence: { value: false }, + disablePresenceControls: { value: false }, + disableFollowMe: { value: false }, + disableFollowParticipant: { value: false }, + disableGoToParticipant: { value: false }, + disableGatherAll: { value: false }, + disablePrivateMode: { value: false }, + }); + + expect( + whoIsOnlineComponent['shouldDisableDropdown']({ + activeComponents: ['PresenceButton'], + participantId: 'someId', + }), + ).toEqual(true); + }); + + test('should disable dropdown when disablePresenceControls is true', () => { + whoIsOnlineComponent['useStore'] = jest.fn().mockReturnValue({ + joinedPresence: { value: true }, + disablePresenceControls: { value: true }, + disableFollowMe: { value: false }, + disableFollowParticipant: { value: false }, + disableGoToParticipant: { value: false }, + disableGatherAll: { value: false }, + disablePrivateMode: { value: false }, + }); + + expect( + whoIsOnlineComponent['shouldDisableDropdown']({ + activeComponents: ['PresenceButton'], + participantId: 'someId', + }), + ).toEqual(true); + }); + + test('should disable dropdown for local participant with specific conditions', () => { + whoIsOnlineComponent['useStore'] = jest.fn().mockReturnValue({ + joinedPresence: { value: true }, + disablePresenceControls: { value: false }, + disableFollowMe: { value: true }, + disablePrivateMode: { value: true }, + disableGatherAll: { value: true }, + disableFollowParticipant: { value: true }, + disableGoToParticipant: { value: true }, + }); + + expect( + whoIsOnlineComponent['shouldDisableDropdown']({ + activeComponents: ['PresenceButton'], + participantId: 'localParticipantId', + }), + ).toEqual(true); + }); + + test('should not disable dropdown when conditions are not met', () => { + whoIsOnlineComponent['useStore'] = jest.fn().mockReturnValue({ + joinedPresence: { value: true }, + disablePresenceControls: { value: false }, + disableFollowMe: { value: false }, + disableFollowParticipant: { value: false }, + disableGoToParticipant: { value: false }, + disableGatherAll: { value: false }, + disablePrivateMode: { value: false }, + }); + + expect( + whoIsOnlineComponent['shouldDisableDropdown']({ + activeComponents: ['presence'], + participantId: 'someId', + }), + ).toEqual(false); + }); + + test('should not disable dropdown when activeComponents do not match', () => { + whoIsOnlineComponent['useStore'] = jest.fn().mockReturnValue({ + joinedPresence: { value: true }, + disablePresenceControls: { value: false }, + disableFollowMe: { value: false }, + disableFollowParticipant: { value: false }, + disableGoToParticipant: { value: false }, + disableGatherAll: { value: false }, + disablePrivateMode: { value: false }, + }); + + expect( + whoIsOnlineComponent['shouldDisableDropdown']({ + activeComponents: ['OtherComponent'], + participantId: 'someId', + }), + ).toEqual(true); + }); + }); + + describe('getTooltipData', () => { + test('should return tooltip data for local participant', () => { + const tooltipData = whoIsOnlineComponent['getTooltipData']({ + isLocalParticipant: true, + name: 'John', + presenceEnabled: true, + }); + + expect(tooltipData).toEqual({ + name: 'John (You)', + }); + }); + + test('should return tooltip data for remote participant with presence enabled', () => { + const tooltipData = whoIsOnlineComponent['getTooltipData']({ + isLocalParticipant: false, + name: 'Alice', + presenceEnabled: true, + }); + + expect(tooltipData).toEqual({ + name: 'Alice', + info: 'Click to follow', + }); + }); + + test('should return tooltip data for remote participant with presence disabled', () => { + const tooltipData = whoIsOnlineComponent['getTooltipData']({ + isLocalParticipant: false, + name: 'Bob', + presenceEnabled: false, + }); + + expect(tooltipData).toEqual({ + name: 'Bob', + }); + }); + + test('should return tooltip data for local participant with presence disabled', () => { + const tooltipData = whoIsOnlineComponent['getTooltipData']({ + isLocalParticipant: true, + name: 'Jane', + presenceEnabled: false, + }); + + expect(tooltipData).toEqual({ + name: 'Jane (You)', + }); + }); + }); + + describe('getAvatar', () => { + const mockAvatar: Avatar = { + imageUrl: 'https://example.com/avatar.jpg', + color: 'white', + firstLetter: 'L', + slotIndex: 0, + }; + + test('should return avatar data with image URL', () => { + const result = whoIsOnlineComponent['getAvatar']({ + avatar: mockAvatar as any, + name: 'John Doe', + color: '#007bff', + slotIndex: 1, + }); + + expect(result).toEqual({ + imageUrl: 'https://example.com/avatar.jpg', + firstLetter: 'J', + color: '#007bff', + slotIndex: 1, + }); + }); + + test('should return avatar data with default first letter', () => { + const result = whoIsOnlineComponent['getAvatar']({ + avatar: { + imageUrl: '', + model3DUrl: '', + }, + name: 'Alice Smith', + color: '#dc3545', + slotIndex: 2, + }); + + expect(result).toEqual({ + imageUrl: '', + firstLetter: 'A', + color: '#dc3545', + slotIndex: 2, + }); + }); + + test('should handle empty name by defaulting to "A"', () => { + const result = whoIsOnlineComponent['getAvatar']({ + avatar: mockAvatar as any, + name: 'User name', + color: '#28a745', + slotIndex: 3, + }); + + expect(result).toEqual({ + imageUrl: 'https://example.com/avatar.jpg', + firstLetter: 'U', + color: '#28a745', + slotIndex: 3, + }); + }); + + test('should handle undefined name by defaulting to "A"', () => { + const result = whoIsOnlineComponent['getAvatar']({ + avatar: { + imageUrl: '', + model3DUrl: '', + }, + name: '', + color: '#ffc107', + slotIndex: 4, + }); + + expect(result).toEqual({ + imageUrl: '', + firstLetter: 'A', + color: '#ffc107', + slotIndex: 4, + }); + }); + }); + + describe('getControls', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return undefined when presence controls are disabled', () => { + const { disablePresenceControls } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disablePresenceControls.publish(true); + + const controls = whoIsOnlineComponent['getControls']({ + participantId: 'remoteParticipant123', + presenceEnabled: true, + }); + + expect(controls).toBeUndefined(); + }); + + test('should return controls for local participant', () => { + const { disablePresenceControls } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disablePresenceControls.publish(false); + + const controls = whoIsOnlineComponent['getControls']({ + participantId: MOCK_LOCAL_PARTICIPANT.id, + presenceEnabled: true, + }); + + expect(controls).toEqual([ + { label: 'gather all', icon: 'gather' }, + { icon: 'send', label: 'everyone follows me' }, + { icon: 'eye', label: 'private mode' }, + ]); + }); + + test('should return controls for other participants', () => { + const { disablePresenceControls } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disablePresenceControls.publish(false); + + const getOtherParticipantsControlsMock = jest.fn().mockReturnValue([ + { label: 'gather all', icon: 'gather' }, + { icon: 'send', label: 'everyone follows me' }, + { icon: 'eye', label: 'private mode' }, + ]); + + const controls = whoIsOnlineComponent['getControls']({ + participantId: 'remoteParticipant456', + presenceEnabled: true, + }); + + expect(controls).toEqual([ + { icon: 'place', label: 'go to' }, + { label: 'follow', icon: 'send' }, + ]); + }); + }); + + describe('getOtherParticipantsControls', () => { + test('should return controls without "Go To" option when disableGoToParticipant is true', () => { + const { disableGoToParticipant, disableFollowParticipant, following } = whoIsOnlineComponent[ + 'useStore' + ](StoreType.WHO_IS_ONLINE); + disableGoToParticipant.publish(true); + disableFollowParticipant.publish(false); + following.publish(undefined); + + const controls = whoIsOnlineComponent['getOtherParticipantsControls']('participant123'); + + expect(controls).toEqual([ + { + label: 'follow', + icon: 'send', + }, + ]); + }); + + test('should return controls with "Go To" option when disableGoToParticipant is false', () => { + const { disableGoToParticipant, disableFollowParticipant, following } = whoIsOnlineComponent[ + 'useStore' + ](StoreType.WHO_IS_ONLINE); + disableGoToParticipant.publish(false); + disableFollowParticipant.publish(false); + following.publish(undefined); + + const controls = whoIsOnlineComponent['getOtherParticipantsControls']('participant123'); + + expect(controls).toEqual([ + { + label: 'go to', + icon: 'place', + }, + { + label: 'follow', + icon: 'send', + }, + ]); + }); + + test('should return controls for when following a participant', () => { + const { disableGoToParticipant, disableFollowParticipant, following } = whoIsOnlineComponent[ + 'useStore' + ](StoreType.WHO_IS_ONLINE); + disableGoToParticipant.publish(false); + disableFollowParticipant.publish(false); + following.publish({ color: 'red', id: 'participant123', name: 'name' }); + + const controls = whoIsOnlineComponent['getOtherParticipantsControls']('participant123'); + + expect(controls).toEqual([ + { + label: 'go to', + icon: 'place', + }, + { + label: 'unfollow', + icon: 'send-off', + }, + ]); + }); + + test('should return controls when disableFollowParticipant is true', () => { + const { disableGoToParticipant, disableFollowParticipant, following } = whoIsOnlineComponent[ + 'useStore' + ](StoreType.WHO_IS_ONLINE); + disableGoToParticipant.publish(false); + disableFollowParticipant.publish(true); + following.publish(undefined); + + const controls = whoIsOnlineComponent['getOtherParticipantsControls']('participant123'); + + expect(controls).toEqual([ + { + label: 'go to', + icon: 'place', + }, + ]); + }); + }); + + describe('getLocalParticipantControls', () => { + test('should return controls without "Gather" option when disableGatherAll is true', () => { + const { + disableFollowMe, + disableGatherAll, + disablePrivateMode, + everyoneFollowsMe, + privateMode, + } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disableFollowMe.publish(false); + disableGatherAll.publish(true); + disablePrivateMode.publish(false); + everyoneFollowsMe.publish(false); + privateMode.publish(false); + + const controls = whoIsOnlineComponent['getLocalParticipantControls'](); + + expect(controls).toEqual([ + { + label: 'everyone follows me', + icon: 'send', + }, + { + label: 'private mode', + icon: 'eye', + }, + ]); + }); + + test('should return controls with "Unfollow" and "Leave Private" options when everyoneFollowsMe and privateMode are true', () => { + const { + disableFollowMe, + disableGatherAll, + disablePrivateMode, + everyoneFollowsMe, + privateMode, + } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disableFollowMe.publish(false); + disableGatherAll.publish(false); + disablePrivateMode.publish(false); + everyoneFollowsMe.publish(true); + privateMode.publish(true); + + const controls = whoIsOnlineComponent['getLocalParticipantControls'](); + + expect(controls).toEqual([ + { + icon: 'gather', + label: 'gather all', + }, + { + icon: 'send-off', + label: 'stop followers', + }, + { + icon: 'eye_inative', + label: 'leave private mode', + }, + ]); + }); + + test('should return controls with "Follow" and "Private" options when all flags are false', () => { + const { + disableFollowMe, + disableGatherAll, + disablePrivateMode, + everyoneFollowsMe, + privateMode, + } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disableFollowMe.publish(false); + disableGatherAll.publish(false); + disablePrivateMode.publish(false); + everyoneFollowsMe.publish(false); + privateMode.publish(false); + + const controls = whoIsOnlineComponent['getLocalParticipantControls'](); + + expect(controls).toEqual([ + { + label: 'gather all', + icon: 'gather', + }, + { + label: 'everyone follows me', + icon: 'send', + }, + { + label: 'private mode', + icon: 'eye', + }, + ]); + }); + + test('should return controls without "Follow" option when disableFollowMe is true', () => { + const { + disableFollowMe, + disableGatherAll, + disablePrivateMode, + everyoneFollowsMe, + privateMode, + } = whoIsOnlineComponent['useStore'](StoreType.WHO_IS_ONLINE); + disableFollowMe.publish(true); + disableGatherAll.publish(false); + disablePrivateMode.publish(false); + everyoneFollowsMe.publish(false); + privateMode.publish(false); + + const controls = whoIsOnlineComponent['getLocalParticipantControls'](); + + expect(controls).toEqual([ + { + label: 'gather all', + icon: 'gather', + }, + { + label: 'private mode', + icon: 'eye', + }, + ]); + }); + }); + + describe('highlightParticipantBeingFollowed', () => { + test('should put participant being followed from extras in second position over all', () => { + const participant1 = { + activeComponents: [], + avatar: {} as Avatar, + id: 'test id 1', + isLocalParticipant: false, + name: 'participant', + tooltip: {} as TooltipData, + controls: {} as any, + disableDropdown: false, + }; + + const participant2 = { ...participant1, id: 'test id 2' }; + const participant3 = { ...participant1, id: 'test id 3' }; + const participant4 = { ...participant1, id: 'test id 4' }; + const participant5 = { ...participant1, id: 'test id 5' }; + + const participantsList: Participant[] = [ + participant1, + participant2, + participant3, + participant4, + ]; + + const { participants, extras, following } = whoIsOnlineComponent['useStore']( + StoreType.WHO_IS_ONLINE, + ); + + participants.publish(participantsList); + extras.publish([participant5]); + following.publish({ + color: 'red', + id: 'test id 5', + name: 'participant 5', + }); + + expect(participants.value[0]).toBe(participant1); + expect(participants.value[1]).toBe(participant2); + expect(participants.value[2]).toBe(participant3); + expect(participants.value[3]).toBe(participant4); + expect(extras.value[0]).toBe(participant5); + + whoIsOnlineComponent['highlightParticipantBeingFollowed'](); + + expect(participants.value[0]).toBe(participant1); + expect(participants.value[1]).toBe(participant5); + expect(participants.value[2]).toBe(participant2); + expect(participants.value[3]).toBe(participant3); + expect(extras.value[0]).toBe(participant4); + }); + }); }); diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index a4fd4e3d..71c85278 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -35,14 +35,6 @@ export class WhoIsOnline extends BaseComponent { this.name = ComponentNames.WHO_IS_ONLINE; this.logger = new Logger('@superviz/sdk/who-is-online-component'); - if (typeof options !== 'object') { - this.position = options ?? Position.TOP_RIGHT; - return; - } - - this.position = options.position ?? Position.TOP_RIGHT; - this.setStyles(options.styles); - const { disablePresenceControls, disableGoToParticipant, @@ -53,14 +45,24 @@ export class WhoIsOnline extends BaseComponent { following, } = this.useStore(StoreType.WHO_IS_ONLINE); - disablePresenceControls.publish(options.flags?.disablePresenceControls); - disableGoToParticipant.publish(options.flags?.disableGoToParticipant); - disableFollowParticipant.publish(options.flags?.disableFollowParticipant); - disablePrivateMode.publish(options.flags?.disablePrivateMode); - disableGatherAll.publish(options.flags?.disableGatherAll); - disableFollowMe.publish(options.flags?.disableFollowMe); - following.subscribe(); + + if (typeof options !== 'object') { + this.position = options ?? Position.TOP_RIGHT; + return; + } + + if (typeof options === 'object') { + this.position = options.position ?? Position.TOP_RIGHT; + this.setStyles(options.styles); + + disablePresenceControls.publish(options.flags?.disablePresenceControls); + disableGoToParticipant.publish(options.flags?.disableGoToParticipant); + disableFollowParticipant.publish(options.flags?.disableFollowParticipant); + disablePrivateMode.publish(options.flags?.disablePrivateMode); + disableGatherAll.publish(options.flags?.disableGatherAll); + disableFollowMe.publish(options.flags?.disableFollowMe); + } } /** @@ -278,6 +280,12 @@ export class WhoIsOnline extends BaseComponent { } }; + /** + * @function setFollow + * @description Sets participant being followed after someone used Everyone Follows Me + * @param followingData + * @returns + */ private setFollow = (followingData: AblyParticipant) => { if (followingData.clientId === this.localParticipantId) return; @@ -305,6 +313,13 @@ export class WhoIsOnline extends BaseComponent { this.updateParticipantsControls(detail?.id); }; + /** + * @function stopFollowing + * @description Stops following a participant + * @param {AblyParticipant} participant The message sent from Ably (in case of being called as a callback) + * @param {boolean} stopEvent A flag that stops the "stop following" event from being published to the user + * @returns + */ private stopFollowing = (participant: { clientId: string }, stopEvent?: boolean) => { if (participant.clientId !== this.following?.id) return; @@ -318,12 +333,23 @@ export class WhoIsOnline extends BaseComponent { this.publish(WhoIsOnlineEvent.STOP_FOLLOWING_PARTICIPANT); }; + /** + * @function gather + * @description Propagates the gather all event in the room + * @param {CustomEvent} data The custom event object containing data about the participant calling for the gather all + */ private gather = (data: CustomEvent) => { this.realtime.setGatherWIOParticipant({ ...data.detail }); this.publish(WhoIsOnlineEvent.GATHER_ALL, data.detail.id); }; - private filterParticipants(participants: AblyParticipant[]) { + /** + * @function filterParticipants + * @description Removes all participants in the room that shouldn't be shown as part of the Who Is Online component, either because they are private, or because they don't have the component active + * @param {AblyParticipant[]} participants The list of participants that will be filtered + * @returns {AblyParticipant[]} + */ + private filterParticipants(participants: AblyParticipant[]): AblyParticipant[] { return participants.filter(({ data: { activeComponents, id, isPrivate } }) => { if (isPrivate && this.localParticipantId !== id) { this.stopFollowing(id, true); @@ -344,6 +370,12 @@ export class WhoIsOnline extends BaseComponent { }); } + /** + * @function getParticipant + * @description Accomodates the data from a participant coming from Ably to something used in Who Is Online. + * @param {AblyParticipant} participant The participant that will be analyzed + * @returns {Participant} The data that will be used in the Who Is Online component the most + */ private getParticipant(participant: AblyParticipant): Participant { const { avatar: avatarLinks, activeComponents, participantId, name } = participant.data; const isLocalParticipant = participant.clientId === this.localParticipantId; @@ -368,6 +400,12 @@ export class WhoIsOnline extends BaseComponent { }; } + /** + * @function shouldDisableDropdown + * @description Decides whether the dropdown with presence controls should be available in a given participant, varying whether they have a presence control enabled or not + * @param {activeComponents: string[] | undefined; participantId: string;} data Info regarding the participant that will be used to decide if their avatar will be clickable + * @returns {boolean} True or false depending if should disable the participant dropdown or not + */ private shouldDisableDropdown({ activeComponents, participantId, @@ -376,7 +414,7 @@ export class WhoIsOnline extends BaseComponent { participantId: string; }) { const { - joinedPresence, + joinedPresence: { value: joinedPresence }, disablePresenceControls: { value: disablePresenceControls }, disableFollowMe: { value: disableFollowMe }, disableFollowParticipant: { value: disableFollowParticipant }, @@ -386,8 +424,8 @@ export class WhoIsOnline extends BaseComponent { } = this.useStore(StoreType.WHO_IS_ONLINE); if ( - joinedPresence.value === false || - disablePresenceControls.value === true || + joinedPresence === false || + disablePresenceControls === true || (participantId === this.localParticipantId && disableFollowMe && disablePrivateMode && @@ -401,6 +439,12 @@ export class WhoIsOnline extends BaseComponent { return !activeComponents?.some((component) => component.toLowerCase().includes('presence')); } + /** + * @function getTooltipData + * @description Processes the participant info and discovers how the tooltip message should looking when hovering over their avatars + * @param {isLocalParticipant: boolean; name: string; presenceEnabled: boolean } data Relevant info about the participant that will be used to decide + * @returns {TooltipData} What the participant tooltip will look like + */ private getTooltipData({ isLocalParticipant, name, @@ -423,6 +467,12 @@ export class WhoIsOnline extends BaseComponent { return data; } + /** + * @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 + * @returns {Avatar} Information used to decide how to construct the participant's avatar html + */ private getAvatar({ avatar, color, @@ -435,11 +485,17 @@ export class WhoIsOnline extends BaseComponent { slotIndex: number; }) { const imageUrl = avatar?.imageUrl; - const firstLetter = name?.at(0).toUpperCase() ?? 'A'; + const firstLetter = name?.at(0)?.toUpperCase() ?? 'A'; return { imageUrl, firstLetter, color, slotIndex }; } + /** + * @function getControls + * @description Decides which presence controls the user should see when opening a participant dropdown + * @param { participantId: string; presenceEnabled: boolean } data Relevant info about the participant that will be used to decide + * @returns {DropdownOption[]} The presence controls enabled for a given participant + */ private getControls({ participantId, presenceEnabled, @@ -458,6 +514,12 @@ export class WhoIsOnline extends BaseComponent { return this.getOtherParticipantsControls(participantId); } + /** + * @function getOtherParticipantsControls + * @description Decides which presence controls the user should see when opening the dropdown of a participant that is not the local participant + * @param {string} participantId Which participant is being analyzed + * @returns {DropdownOption[]} The presence controls enabled for the participant + */ private getOtherParticipantsControls(participantId: string): DropdownOption[] { const { disableGoToParticipant, disableFollowParticipant, following } = this.useStore( StoreType.WHO_IS_ONLINE, @@ -481,6 +543,11 @@ export class WhoIsOnline extends BaseComponent { return controls; } + /** + * @function getLocalParticipantControls + * @description Decides which presence controls the user should see when opening the dropdown of the local participant + * @returns {DropdownOption[]} The presence controls enabled for the local participant + */ private getLocalParticipantControls(): DropdownOption[] { const { disableFollowMe: { value: disableFollowMe }, @@ -517,6 +584,12 @@ export class WhoIsOnline extends BaseComponent { return controls; } + /** + * @function setParticipants + * @description Adds participants to the main participants (the 4 that are shown without opening any dropdown) until the list is full + * @param {Participant[]} participantsList The total participants list + * @returns {Participant[]} The participants that did not fit the main list and will be inserted in the extras participants list + */ private setParticipants = (participantsList: Participant[]): Participant[] => { const { participants } = this.useStore(StoreType.WHO_IS_ONLINE); @@ -530,12 +603,24 @@ export class WhoIsOnline extends BaseComponent { return participantsList; }; - private setExtras = (participantsList: Participant[]) => { + /** + * @function setExtras + * @description Adds remaining participants to extras participants (those who are shown without opening any dropdown) + * @param {Participant[]} participantsList The remaining participants list + * @returns {void} + */ + private setExtras = (participantsList: Participant[]): void => { const { extras } = this.useStore(StoreType.WHO_IS_ONLINE); extras.publish(participantsList); }; - private updateParticipantsControls(participantId: string | undefined) { + /** + * @function updateParticipantsControls + * @description Updated what the presence controls of a single participant should look like now that something about them was updated + * @param {string | undefined} participantId The participant that suffered some update + * @returns {void} The participants that did not fit the main list and will be inserted in the extras participants list + */ + private updateParticipantsControls(participantId: string | undefined): void { const { participants } = this.useStore(StoreType.WHO_IS_ONLINE); participants.publish( @@ -558,7 +643,12 @@ export class WhoIsOnline extends BaseComponent { ); } - private highlightParticipantBeingFollowed() { + /** + * @function highlightParticipantBeingFollowed + * @description Brings a participant that is in the list of extra participants to the front, in the second place of the list of main participants, so they are visible while being followed + * @returns {void} + */ + private highlightParticipantBeingFollowed(): void { const { extras, participants, diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 21dfe3ae..7a275f51 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -1,4 +1,3 @@ -import { Participant as GeneralParticipant } from '../../common/types/participant.types'; import { DropdownOption } from '../../web-components/dropdown/types'; export enum Position { diff --git a/src/web-components/comments/comments.test.ts b/src/web-components/comments/comments.test.ts index dde42b77..cb1a79e0 100644 --- a/src/web-components/comments/comments.test.ts +++ b/src/web-components/comments/comments.test.ts @@ -79,14 +79,14 @@ describe('comments', () => { }); test('should set filter', async () => { - const filter = 'test'; - const detail = { filter }; + const label = 'test'; + const detail = { filter: { label } }; element['setFilter']({ detail }); await sleep(); - expect(element['annotationFilter']).toEqual(filter); + expect(element['annotationFilter']).toEqual(label); }); test('should show water mark', async () => { diff --git a/src/web-components/comments/components/comment-item.test.ts b/src/web-components/comments/components/comment-item.test.ts index 6edd0c37..31071316 100644 --- a/src/web-components/comments/components/comment-item.test.ts +++ b/src/web-components/comments/components/comment-item.test.ts @@ -98,7 +98,9 @@ describe('CommentsCommentItem', () => { await element['updateComplete']; const dropdown = element.shadowRoot!.querySelector('superviz-dropdown') as HTMLElement; - dropdown.dispatchEvent(new CustomEvent('selected', { detail: CommentDropdownOptions.EDIT })); + dropdown.dispatchEvent( + new CustomEvent('selected', { detail: { label: CommentDropdownOptions.EDIT } }), + ); await element['updateComplete']; @@ -110,7 +112,9 @@ describe('CommentsCommentItem', () => { await element['updateComplete']; const dropdown = element.shadowRoot!.querySelector('superviz-dropdown') as HTMLElement; - dropdown.dispatchEvent(new CustomEvent('selected', { detail: CommentDropdownOptions.EDIT })); + dropdown.dispatchEvent( + new CustomEvent('selected', { detail: { label: CommentDropdownOptions.EDIT } }), + ); await element['updateComplete']; @@ -131,7 +135,9 @@ describe('CommentsCommentItem', () => { await element['updateComplete']; const dropdown = element.shadowRoot!.querySelector('superviz-dropdown') as HTMLElement; - dropdown.dispatchEvent(new CustomEvent('selected', { detail: CommentDropdownOptions.EDIT })); + dropdown.dispatchEvent( + new CustomEvent('selected', { detail: { label: CommentDropdownOptions.EDIT } }), + ); await element['updateComplete']; @@ -156,7 +162,9 @@ describe('CommentsCommentItem', () => { await element['updateComplete']; const dropdown = element.shadowRoot!.querySelector('superviz-dropdown') as HTMLElement; - dropdown.dispatchEvent(new CustomEvent('selected', { detail: CommentDropdownOptions.DELETE })); + dropdown.dispatchEvent( + new CustomEvent('selected', { detail: { label: CommentDropdownOptions.DELETE } }), + ); await element['updateComplete']; diff --git a/src/web-components/dropdown/index.test.ts b/src/web-components/dropdown/index.test.ts index 691dbb85..d3a6267b 100644 --- a/src/web-components/dropdown/index.test.ts +++ b/src/web-components/dropdown/index.test.ts @@ -2,26 +2,26 @@ import '.'; import sleep from '../../common/utils/sleep'; +import { DropdownOption } from './types'; + interface elementProps { position: string; align: string; label?: string; - returnTo?: string; - options?: Record; + options?: DropdownOption[]; name?: string; - icons?: string[]; showTooltip?: boolean; + returnData?: any; } export const createEl = ({ position, align, label, - returnTo, options, name, - icons, showTooltip, + returnData, }: elementProps): HTMLElement => { const element: HTMLElement = document.createElement('superviz-dropdown'); @@ -29,20 +29,16 @@ export const createEl = ({ element.setAttribute('label', label); } - if (returnTo) { - element.setAttribute('returnTo', returnTo); - } - if (options) { element.setAttribute('options', JSON.stringify(options)); } - if (name) { - element.setAttribute('name', name); + if (returnData) { + element.setAttribute('returnData', JSON.stringify(returnData)); } - if (icons) { - element.setAttribute('icons', JSON.stringify(icons)); + if (name) { + element.setAttribute('name', name); } if (showTooltip) { @@ -137,7 +133,7 @@ describe('dropdown', () => { }); test('should close dropdown when click on it', async () => { - const el = createEl({ position: 'bottom-left', align: 'left', icons: ['left', 'right'] }); + const el = createEl({ position: 'bottom-left', align: 'left' }); await sleep(); dropdownContent()?.click(); @@ -213,8 +209,8 @@ describe('dropdown', () => { expect(spy).toHaveBeenCalled(); }); - test('should emit event with all content when returnTo not specified', async () => { - createEl({ position: 'bottom-right', align: 'left' }); + test('should emit event with returnData when returnData specified', async () => { + createEl({ position: 'bottom-right', align: 'left', returnData: { value: 1 } }); await sleep(); @@ -235,10 +231,7 @@ describe('dropdown', () => { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ detail: { - name: 'EDIT', - value: { - uuid: 'any_uuid', - }, + value: 1, }, }), ); @@ -273,7 +266,14 @@ describe('dropdown', () => { }); test('should show icons if icons is specified', async () => { - createEl({ position: 'bottom-right', align: 'left', icons: ['left', 'right'] }); + createEl({ + position: 'bottom-right', + align: 'left', + options: [ + { label: 'left', icon: 'left' }, + { label: 'right', icon: 'right' }, + ], + }); await sleep(); @@ -282,36 +282,6 @@ describe('dropdown', () => { expect(icons).not.toBeNull(); }); - test('should emit event with returnTo content when returnTo specified', async () => { - createEl({ position: 'bottom-right', align: 'left', label: 'name', returnTo: 'value' }); - - await sleep(); - - element()!['emitEvent'] = jest.fn(); - - dropdownContent()?.click(); - - await sleep(); - - const option = dropdownListUL()?.querySelector('li'); - - option?.click(); - - await sleep(); - - expect(element()!['emitEvent']).toHaveBeenNthCalledWith( - 3, - 'selected', - { - uuid: 'any_uuid', - }, - { - bubbles: false, - composed: true, - }, - ); - }); - describe('tooltip', () => { test('should render tooltip if can show it', async () => { createEl({ position: 'bottom-right', align: 'left', showTooltip: true }); diff --git a/src/web-components/tooltip/index.test.ts b/src/web-components/tooltip/index.test.ts index f67c51b2..d1ae8427 100644 --- a/src/web-components/tooltip/index.test.ts +++ b/src/web-components/tooltip/index.test.ts @@ -3,7 +3,7 @@ import sleep from '../../common/utils/sleep'; import '.'; interface Attributes { - tooltipData?: { name: string; action: string }; + tooltipData?: { name: string; info: string }; shiftTooltipLeft?: boolean; } @@ -123,7 +123,7 @@ describe('tooltip', () => { test('should render tooltip with the correct data', async () => { const element = createEl({ - tooltipData: { name: 'test name', action: 'test action' }, + tooltipData: { name: 'test name', info: 'test action' }, }); await sleep(); 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 c3823ef0..9139aa68 100644 --- a/src/web-components/who-is-online/components/dropdown.test.ts +++ b/src/web-components/who-is-online/components/dropdown.test.ts @@ -1,11 +1,12 @@ import '.'; import { MeetingColorsHex } from '../../../common/types/meeting-colors.types'; +import { StoreType } from '../../../common/types/stores.types'; import sleep from '../../../common/utils/sleep'; -import { Participant } from '../../../components/who-is-online/types'; +import { useStore } from '../../../common/utils/use-store'; +import { Participant, WIODropdownOptions } from '../../../components/who-is-online/types'; interface elementProps { position: string; - participants?: Participant[]; label?: string; returnTo?: string; options?: any; @@ -13,27 +14,82 @@ interface elementProps { icons?: string[]; } -const mockParticipants: Participant[] = [ +const MOCK_PARTICIPANTS: Participant[] = [ { + name: 'John Zero', avatar: { - imageUrl: '', - model3DUrl: '', + imageUrl: 'https://example.com', + color: MeetingColorsHex[0], + firstLetter: 'J', + slotIndex: 0, }, - color: MeetingColorsHex[0], id: '1', - name: 'John Zero', - slotIndex: 0, + activeComponents: ['whoisonline', 'presence'], + isLocalParticipant: true, + tooltip: { + name: 'John Zero (you)', + }, + controls: [ + { + label: WIODropdownOptions.GATHER, + }, + { + label: WIODropdownOptions.FOLLOW, + }, + { + label: WIODropdownOptions.PRIVATE, + }, + ], + }, + { + name: 'John Uno', + avatar: { + imageUrl: '', + color: MeetingColorsHex[1], + firstLetter: 'J', + slotIndex: 1, + }, + id: '2', + activeComponents: ['whoisonline'], + isLocalParticipant: false, + tooltip: { + name: 'John Uno', + }, + controls: [ + { + label: WIODropdownOptions.GOTO, + }, + { + label: WIODropdownOptions.LOCAL_FOLLOW, + }, + ], + }, + { + name: 'John Doe', + avatar: { + imageUrl: '', + color: MeetingColorsHex[2], + firstLetter: 'J', + slotIndex: 2, + }, + id: '3', + activeComponents: ['whoisonline', 'presence'], + isLocalParticipant: true, + tooltip: { + name: 'John Doe', + }, + controls: [ + { + label: WIODropdownOptions.GOTO, + }, + { + label: WIODropdownOptions.LOCAL_FOLLOW, + }, + ], }, ]; -const createEl = ({ - position, - label, - returnTo, - name, - icons, - participants, -}: elementProps): HTMLElement => { +const createEl = ({ position, label, returnTo, name, icons }: elementProps): HTMLElement => { const element: HTMLElement = document.createElement('superviz-who-is-online-dropdown'); /* eslint-disable no-unused-expressions */ @@ -41,7 +97,6 @@ const createEl = ({ returnTo && element.setAttribute('returnTo', returnTo); name && element.setAttribute('name', name); icons && element.setAttribute('icons', JSON.stringify(icons)); - !!participants?.length && element.setAttribute('participants', JSON.stringify(participants)); /* eslint-enable no-unused-expressions */ element.setAttribute('position', position); @@ -91,14 +146,19 @@ describe('who-is-online-dropdown', () => { }); test('should render dropdown', () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); + const element = document.querySelector('superviz-who-is-online-dropdown'); expect(element).not.toBeNull(); }); test('should open dropdown when click on it', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); @@ -110,7 +170,9 @@ describe('who-is-online-dropdown', () => { }); test('should close dropdown when click on it', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); dropdownContent()?.click(); @@ -130,7 +192,9 @@ describe('who-is-online-dropdown', () => { }); test('should open another dropdown when click on participant', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); @@ -151,7 +215,9 @@ describe('who-is-online-dropdown', () => { }); test('should listen click event when click out', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); @@ -171,13 +237,15 @@ describe('who-is-online-dropdown', () => { }); test('should give a black color to the letter when the slotIndex is not in the textColorValues', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + 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[mockParticipants[0].slotIndex as number]; + const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[2].avatar.slotIndex as number]; expect(letter?.getAttribute('style')).toBe( `background-color: ${backgroundColor}; color: #26242A`, ); @@ -185,18 +253,19 @@ describe('who-is-online-dropdown', () => { test('should give a white color to the letter when the slotIndex is in the textColorValues', async () => { const participant = { - ...mockParticipants[0], + ...MOCK_PARTICIPANTS[0], slotIndex: 1, color: MeetingColorsHex[1], }; - createEl({ position: 'bottom', participants: [participant] }); - + 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[1]; + const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[1].avatar.slotIndex as number]; expect(letter?.getAttribute('style')).toBe( `background-color: ${backgroundColor}; color: #FFFFFF`, ); @@ -204,13 +273,17 @@ describe('who-is-online-dropdown', () => { test('should not render participants when there is no participant', async () => { createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish([]); await sleep(); expect(dropdownMenu()?.children?.length).toBe(0); }); test('should render participants when there is participant', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish([MOCK_PARTICIPANTS[0]]); await sleep(); @@ -218,22 +291,27 @@ describe('who-is-online-dropdown', () => { }); test('should change selected participant when click on it', async () => { - createEl({ - position: 'bottom', - participants: [ - { - avatar: { - imageUrl: '', - model3DUrl: '', - }, - color: MeetingColorsHex[0], - id: '1', - name: 'John Zero', + createEl({ position: 'bottom' }); + + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish([ + { + avatar: { + imageUrl: '', + color: 'red', + firstLetter: 'J', slotIndex: 0, - joinedPresence: true, }, - ], - }); + id: '1', + name: 'John Zero', + activeComponents: ['whoisonline', 'presence'], + isLocalParticipant: false, + tooltip: { + name: 'John', + }, + }, + ]); await sleep(); @@ -245,11 +323,18 @@ describe('who-is-online-dropdown', () => { await sleep(); - expect(element()?.['selected']).toBe(mockParticipants[0].id); + expect(element()?.['selected']).toBe(MOCK_PARTICIPANTS[0].id); }); - test('should not change selected participant when click on it if not in presence', async () => { - createEl({ position: 'bottom', participants: mockParticipants }); + test('should not change selected participant when click on it if disableDropdown is true', async () => { + createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish([ + { + ...MOCK_PARTICIPANTS[0], + disableDropdown: true, + }, + ]); await sleep(); @@ -261,12 +346,15 @@ describe('who-is-online-dropdown', () => { await sleep(); - expect(element()?.['selected']).not.toBe(mockParticipants[0].id); + expect(element()?.['selected']).not.toBe(MOCK_PARTICIPANTS[0].id); }); describe('repositionDropdown', () => { test('should call reposition methods if is open', () => { - const el = createEl({ position: 'bottom', participants: mockParticipants }); + const el = createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); + el['open'] = true; el['repositionInVerticalDirection'] = jest.fn(); @@ -279,7 +367,10 @@ describe('who-is-online-dropdown', () => { }); test('should do nothing if is not open', () => { - const el = createEl({ position: 'bottom', participants: mockParticipants }); + const el = createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); + el['open'] = false; el['repositionInVerticalDirection'] = jest.fn(); @@ -292,22 +383,6 @@ describe('who-is-online-dropdown', () => { }); }); - /** - * private repositionInVerticalDirection = () => { - const { bottom, top, height } = this.parentElement.getBoundingClientRect(); - const windowVerticalMidpoint = window.innerHeight / 2; - const dropdownVerticalMidpoint = top + height / 2; - - if (dropdownVerticalMidpoint > windowVerticalMidpoint) { - this.dropdownList.style.setProperty('bottom', `${window.innerHeight - top + 8}px`); - this.dropdownList.style.setProperty('top', ''); - return; - } - - this.dropdownList.style.setProperty('top', `${bottom + 8}px`); - this.dropdownList.style.setProperty('bottom', ''); - }; - */ describe('repositionInVerticalDirection', () => { beforeEach(() => { document.body.innerHTML = ''; @@ -315,7 +390,9 @@ describe('who-is-online-dropdown', () => { }); test('should set bottom and top styles when dropdownVerticalMidpoint is greater than windowVerticalMidpoint', async () => { - const el = createEl({ position: 'bottom', participants: mockParticipants }); + const el = createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); @@ -336,7 +413,9 @@ describe('who-is-online-dropdown', () => { }); test('should set top and bottom styles when dropdownVerticalMidpoint is less than windowVerticalMidpoint', async () => { - const el = createEl({ position: 'bottom', participants: mockParticipants }); + const el = createEl({ position: 'bottom' }); + const { extras } = useStore(StoreType.WHO_IS_ONLINE); + extras.publish(MOCK_PARTICIPANTS); await sleep(); 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 fbb0504b..55975087 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 @@ -1,18 +1,14 @@ -import { html } from 'lit'; - import '.'; -import { - MOCK_ABLY_PARTICIPANT_DATA_1, - MOCK_LOCAL_PARTICIPANT, -} from '../../../__mocks__/participants.mock'; + +import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { RealtimeEvent } from '../../common/types/events.types'; import { MeetingColorsHex } from '../../common/types/meeting-colors.types'; +import { StoreType } from '../../common/types/stores.types'; import sleep from '../../common/utils/sleep'; -import { Participant } from '../../components/who-is-online/types'; +import { useStore } from '../../common/utils/use-store'; +import { Participant, WIODropdownOptions } from '../../components/who-is-online/types'; import { useGlobalStore } from '../../services/stores'; -import { Dropdown } from '../dropdown/index'; - -import { WIODropdownOptions } from './components/types'; +import { Following } from '../../services/stores/who-is-online/types'; let element: HTMLElement; @@ -20,34 +16,74 @@ const MOCK_PARTICIPANTS: Participant[] = [ { name: 'John Zero', avatar: { - imageUrl: '', - model3DUrl: '', + imageUrl: 'https://example.com', + color: MeetingColorsHex[0], + firstLetter: 'J', + slotIndex: 0, }, - color: MeetingColorsHex[0], id: '1', - slotIndex: 0, - joinedPresence: true, + activeComponents: ['whoisonline', 'presence'], + isLocalParticipant: true, + tooltip: { + name: 'John Zero (you)', + }, + controls: [ + { + label: WIODropdownOptions.GATHER, + }, + { + label: WIODropdownOptions.FOLLOW, + }, + { + label: WIODropdownOptions.PRIVATE, + }, + ], }, { name: 'John Uno', avatar: { - imageUrl: 'https://link.com/image', - model3DUrl: '', + imageUrl: '', + color: MeetingColorsHex[1], + firstLetter: 'J', + slotIndex: 1, }, - color: MeetingColorsHex[1], id: '2', - slotIndex: 1, - isLocal: true, + activeComponents: ['whoisonline'], + isLocalParticipant: false, + tooltip: { + name: 'John Uno', + }, + controls: [ + { + label: WIODropdownOptions.GOTO, + }, + { + label: WIODropdownOptions.LOCAL_FOLLOW, + }, + ], }, { name: 'John Doe', avatar: { imageUrl: '', - model3DUrl: '', + color: MeetingColorsHex[2], + firstLetter: 'J', + slotIndex: 2, }, - color: MeetingColorsHex[2], id: '3', - slotIndex: 2, + activeComponents: ['whoisonline', 'presence'], + isLocalParticipant: true, + tooltip: { + name: 'John Doe', + }, + controls: [ + { + label: WIODropdownOptions.GOTO, + }, + { + label: WIODropdownOptions.LOCAL_FOLLOW, + }, + ], }, ]; @@ -55,6 +91,8 @@ describe('Who Is Online', () => { beforeEach(async () => { const { localParticipant } = useGlobalStore(); localParticipant.value = MOCK_LOCAL_PARTICIPANT; + const { participants } = useStore(StoreType.WHO_IS_ONLINE); + participants.publish([]); element = document.createElement('superviz-who-is-online'); element['localParticipantData'] = { @@ -71,10 +109,10 @@ describe('Who Is Online', () => { }); test('should render a participants with class "who-is-online__participant-list"', async () => { - element['updateParticipants'](MOCK_PARTICIPANTS); - await sleep(); - const participants = element?.shadowRoot?.querySelector('.who-is-online__participant-list'); - expect(participants).not.toBeFalsy(); + const { participants } = useStore(StoreType.WHO_IS_ONLINE); + participants.publish(MOCK_PARTICIPANTS); + const participantsDivs = element?.shadowRoot?.querySelector('.who-is-online__participant-list'); + expect(participantsDivs).not.toBeFalsy(); }); test('should have default positioning style', () => { @@ -109,33 +147,36 @@ describe('Who Is Online', () => { }); test('should update participants list', async () => { - let participants = element?.shadowRoot?.querySelectorAll('.who-is-online__participant'); + const { participants } = useStore(StoreType.WHO_IS_ONLINE); + let participantsDivs = element?.shadowRoot?.querySelectorAll('.who-is-online__participant'); + expect(participantsDivs?.length).toBe(0); - expect(participants?.length).toBe(0); + participants.publish(MOCK_PARTICIPANTS); - element['updateParticipants'](MOCK_PARTICIPANTS); await sleep(); - participants = element.shadowRoot?.querySelectorAll('.who-is-online__participant'); - expect(participants?.length).toBe(3); + participantsDivs = element.shadowRoot?.querySelectorAll('.who-is-online__participant'); + expect(participantsDivs?.length).toBe(3); - element['updateParticipants'](MOCK_PARTICIPANTS.slice(0, 2)); + participants.publish(MOCK_PARTICIPANTS.slice(0, 2)); await sleep(); - participants = element.shadowRoot?.querySelectorAll('.who-is-online__participant'); + participantsDivs = element.shadowRoot?.querySelectorAll('.who-is-online__participant'); - expect(participants?.length).toBe(2); + expect(participantsDivs?.length).toBe(2); }); test('should render excess participants dropdown icon', async () => { - element['updateParticipants'](MOCK_PARTICIPANTS); + const { participants, extras } = useStore(StoreType.WHO_IS_ONLINE); + participants.publish(MOCK_PARTICIPANTS); + await sleep(); let extraParticipants = element?.shadowRoot?.querySelector('.superviz-who-is-online__excess'); - expect(extraParticipants).toBeFalsy(); + expect(extraParticipants).toBe(null); - element['updateParticipants']([...MOCK_PARTICIPANTS, ...MOCK_PARTICIPANTS]); + extras.publish(MOCK_PARTICIPANTS); await sleep(); extraParticipants = element?.shadowRoot?.querySelector('.superviz-who-is-online__excess'); @@ -144,30 +185,25 @@ describe('Who Is Online', () => { }); test('should give a black color to the letter when the slotIndex is not in the textColorValues', async () => { - element['updateParticipants'](MOCK_PARTICIPANTS.slice(0, 1)); + 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[0].slotIndex as number]; - + 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], - }; - - element['updateParticipants']([participant]); + 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[participant.slotIndex]; + const backgroundColor = MeetingColorsHex[MOCK_PARTICIPANTS[1].avatar.slotIndex as number]; expect(letter?.getAttribute('style')).toBe( `background-color: ${backgroundColor}; color: #FFFFFF`, ); @@ -184,14 +220,15 @@ describe('Who Is Online', () => { }); test('should update open property when clicking outside', async () => { + const { participants } = useStore(StoreType.WHO_IS_ONLINE); + const event = new CustomEvent('clickout', { detail: { open: false, }, }); - element['updateParticipants']([...MOCK_PARTICIPANTS, ...MOCK_PARTICIPANTS]); - + participants.publish([...MOCK_PARTICIPANTS, ...MOCK_PARTICIPANTS]); await sleep(); const dropdown = element.shadowRoot?.querySelector( 'superviz-who-is-online-dropdown', @@ -202,69 +239,56 @@ describe('Who Is Online', () => { }); test('should correctly display either name letter or image', () => { - const letter = element['getAvatar'](MOCK_PARTICIPANTS[0]); + const letter = element['getAvatar'](MOCK_PARTICIPANTS[1].avatar); expect(letter.strings[0]).not.toContain('img'); const participant = { ...MOCK_PARTICIPANTS[0], - avatar: { - imageUrl: 'https://link.com/image', - model3DUrl: '', - }, }; - const avatar = element['getAvatar'](participant); + const avatar = element['getAvatar'](participant.avatar); expect(avatar.strings[0]).toContain('img'); }); - test('should stop following if already following', async () => { - const event = new CustomEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { - detail: { id: 1, label: WIODropdownOptions.LOCAL_FOLLOW, slotIndex: 0 }, - }); - - element['dropdownOptionsHandler'](event); - expect(element['following']).toBeTruthy(); - - element['dropdownOptionsHandler'](event); - expect(element['following']).toBeFalsy(); - }); + test('should start and stop following', async () => { + const { following, participants } = useStore(StoreType.WHO_IS_ONLINE); + participants.publish(MOCK_PARTICIPANTS); + await sleep(); - test('should bring hidden participant to second position when following them', async () => { - const participants = [ - ...MOCK_PARTICIPANTS, - ...MOCK_PARTICIPANTS, - { - name: 'Test participant', - avatar: { - imageUrl: '', - model3DUrl: '', - }, - color: MeetingColorsHex[10], - id: 'test', - slotIndex: 10, + const event1 = new CustomEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { + detail: { + participantId: '1', + label: WIODropdownOptions.LOCAL_FOLLOW, + source: 'participants', }, - ]; + }); - element['updateParticipants'](participants); + element['dropdownOptionsHandler'](event1); + await sleep(); - expect(element['participants'].findIndex((participant) => participant.id === 'test')).not.toBe( - 1, - ); + expect(following.value).toBeTruthy(); - const event = new CustomEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { - detail: { id: 'test', label: WIODropdownOptions.LOCAL_FOLLOW, slotIndex: 2 }, + const event2 = new CustomEvent(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, { + detail: { + label: WIODropdownOptions.LOCAL_UNFOLLOW, + }, }); - element['dropdownOptionsHandler'](event); + element['dropdownOptionsHandler'](event2); + await sleep(); - expect(element['participants'].findIndex((participant) => participant.id === 'test')).toBe(1); + expect(following.value).toBeFalsy(); }); describe('dropdownOptionsHandler', () => { let dropdown: HTMLElement; + const { participants, extras } = useStore(StoreType.WHO_IS_ONLINE); + beforeEach(async () => { - element['updateParticipants']([...MOCK_PARTICIPANTS, ...MOCK_PARTICIPANTS]); + participants.publish(MOCK_PARTICIPANTS); + extras.publish(MOCK_PARTICIPANTS); await sleep(); + dropdown = element.shadowRoot?.querySelector( 'superviz-who-is-online-dropdown', ) as HTMLElement; @@ -284,19 +308,26 @@ describe('Who Is Online', () => { }); test('should emit event when selecting follow option in dropdown', async () => { + const { following } = useStore(StoreType.WHO_IS_ONLINE); + const event = new CustomEvent('selected', { - detail: { id: 1, label: WIODropdownOptions.LOCAL_FOLLOW, slotIndex: 1 }, + detail: { + participantId: '1', + label: WIODropdownOptions.LOCAL_FOLLOW, + source: 'participants', + }, }); const spy = jest.fn(); element.addEventListener(RealtimeEvent.REALTIME_LOCAL_FOLLOW_PARTICIPANT, spy); - expect(element['following']).toBeUndefined(); + expect(following.value).toBeUndefined(); dropdown.dispatchEvent(event); + await sleep(); expect(spy).toHaveBeenCalledWith(event); - expect(element['following']).toBeDefined(); + expect(following.value).toBeDefined(); }); test('should emit event when selecting unfollow option in dropdown', async () => { @@ -315,15 +346,18 @@ describe('Who Is Online', () => { expect(element['following']).toBeUndefined(); }); - test('should emit event when selecting follow option in dropdown', async () => { + test('should emit event when selecting follow me option in dropdown', async () => { const event = new CustomEvent('selected', { - detail: { id: 1, label: WIODropdownOptions.FOLLOW, slotIndex: 1 }, + detail: { participantId: '1', label: WIODropdownOptions.FOLLOW, source: 'participants' }, }); const spy = jest.fn(); element.addEventListener(RealtimeEvent.REALTIME_FOLLOW_PARTICIPANT, spy); - element['following'] = { id: 1, slotIndex: 1 }; + const { following } = useStore(StoreType.WHO_IS_ONLINE); + following.publish({ color: 'red', id: '1', name: 'John' }); + element['following'] = { participantId: 1, slotIndex: 1 }; + await sleep(); dropdown.dispatchEvent(event); diff --git a/src/web-components/who-is-online/who-is-online.ts b/src/web-components/who-is-online/who-is-online.ts index 0f8aa9b5..77b9488f 100644 --- a/src/web-components/who-is-online/who-is-online.ts +++ b/src/web-components/who-is-online/who-is-online.ts @@ -82,10 +82,6 @@ export class WhoIsOnline extends WebComponentsBaseElement { importStyle.call(this, 'who-is-online'); } - public updateParticipants(data: Participant[]) { - this.participants = data; - } - private toggleOpen() { this.open = !this.open; } From 8cf2cadce39caac9ca353894577732ff1ab3f822 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Apr 2024 14:37:00 -0300 Subject: [PATCH 78/83] fix: adjust mouse coordinates in test --- src/components/presence-mouse/html/index.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/presence-mouse/html/index.test.ts b/src/components/presence-mouse/html/index.test.ts index f4921407..2b2a3d06 100644 --- a/src/components/presence-mouse/html/index.test.ts +++ b/src/components/presence-mouse/html/index.test.ts @@ -458,16 +458,16 @@ describe('MousePointers on HTML', () => { presenceMouseComponent['container'].getBoundingClientRect = jest.fn( () => ({ - x: 50, - y: 50, - width: 100, - height: 100, + x: 10, + y: 10, + width: 10, + height: 10, } as any), ); const mouseEvent1 = { - x: 20, - y: 30, + x: -50, + y: -50, } as any; presenceMouseComponent['onMyParticipantMouseLeave'](mouseEvent1); @@ -476,8 +476,8 @@ describe('MousePointers on HTML', () => { updatePresenceMouseSpy.mockClear(); const mouseEvent2 = { - x: 75, - y: 75, + x: 5, + y: 5, } as any; presenceMouseComponent['onMyParticipantMouseLeave'](mouseEvent2); From 1052fcdbd064daab93e031e067c8a3ad1995b1ea Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Mon, 8 Apr 2024 17:01:36 -0300 Subject: [PATCH 79/83] refactor: remove client sync methods from old io --- __mocks__/realtime.mock.ts | 1 - lib.md | 80 ------------------------ src/components/realtime/index.ts | 8 ++- src/components/realtime/types.ts | 7 +++ src/components/video/index.ts | 5 +- src/index.ts | 8 ++- src/services/realtime/ably/index.test.ts | 10 +-- src/services/realtime/ably/index.ts | 45 ------------- src/services/realtime/ably/types.ts | 7 --- src/services/realtime/base/types.ts | 1 - 10 files changed, 22 insertions(+), 150 deletions(-) delete mode 100644 lib.md diff --git a/__mocks__/realtime.mock.ts b/__mocks__/realtime.mock.ts index 3da803f0..662f8858 100644 --- a/__mocks__/realtime.mock.ts +++ b/__mocks__/realtime.mock.ts @@ -29,7 +29,6 @@ export const ABLY_REALTIME_MOCK: AblyRealtimeService = { setDrawing: jest.fn(), freezeSync: jest.fn(), setParticipantData: jest.fn(), - setSyncProperty: jest.fn(), setKickParticipant: jest.fn(), setTranscript: jest.fn(), start: jest.fn(), diff --git a/lib.md b/lib.md deleted file mode 100644 index a2a87472..00000000 --- a/lib.md +++ /dev/null @@ -1,80 +0,0 @@ -## Initialization example - -```ts -import { SuperViz } from '@superviz/sdk'; -import { - VideoComponent, - Realtime, - PresenceComponent, - CommentsComponent, - CanvasAdapter, -} from '@superviz/sdk/components'; -import { Presence3D, CommentsAdapter } from '@superviz/matterport'; -import { Presence3D, CommentsAdapter } from '@superviz/three'; - -const SuperViz = await Manager(DEVELOPER_KEY, { - roomId: this.roomId, - participant: { - id: this.userId, - name: 'John Doe', - avatar: { - thumbnail: 'https://production.storage.superviz.com/readyplayerme/1.png', - }, - }, -}); - -const video = new VideoComponent({ - userType: 'host' | 'guest' | 'audience', -}); -SuperViz.addComponent(video); - -const realtime = new Realtime(); -SuperViz.addComponent(realtime); - -const mpPresence = new Presence3D(mpInstance); -SuperViz.addComponent(mpPresence); - -// Initialize comments with canvas -const canvasAdapter = new CanvasAdapter('canvas-id'); -const comments = new CommentsComponent(canvasAdapter); -SuperViz.addComponent(comments); - -// Initialize coments with matterport 3d component -const mpCommentsAdapter = new CommentsAdapter(mpInstance); -const comments = new CommentsComponent(mpCommentsAdapter); -SuperViz.addComponent(comments); -SuperViz.addComponent(mpPresence); - -// PubSub -realtime.unsubscribe('presence-updated', () => {}); -realtime.subscribe('presence-updated', () => {}); -realtime.publish('presence-updated', {}); - -// removing component -SuperViz.removeComponent(mpPresence); -``` - -### Folder strucuture - -``` -- sdk - - src - - common - - utils - - types - - components - - comments - - presence - - video - - web components - - comments - - presence - - services - - realtime - - api - - auth - - core - - manager.ts - - pubsub.ts - - index.ts -``` diff --git a/src/components/realtime/index.ts b/src/components/realtime/index.ts index 63225f29..260df168 100644 --- a/src/components/realtime/index.ts +++ b/src/components/realtime/index.ts @@ -4,12 +4,16 @@ import throttle from 'lodash/throttle'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { StoreType } from '../../common/types/stores.types'; import { Logger, Observer } from '../../common/utils'; -import { RealtimeMessage } from '../../services/realtime/ably/types'; import { BaseComponent } from '../base'; import { ComponentNames } from '../types'; import { Participant } from '../who-is-online/types'; -import { RealtimeComponentEvent, RealtimeComponentState, RealtimeData } from './types'; +import { + RealtimeComponentEvent, + RealtimeComponentState, + RealtimeData, + RealtimeMessage, +} from './types'; export class Realtime extends BaseComponent { private callbacksToSubscribeWhenJoined: Array<{ diff --git a/src/components/realtime/types.ts b/src/components/realtime/types.ts index b2a9e80f..497acccf 100644 --- a/src/components/realtime/types.ts +++ b/src/components/realtime/types.ts @@ -11,3 +11,10 @@ export type RealtimeData = { name: string; payload: any; }; + +export type RealtimeMessage = { + name: string; + participantId: string; + data: unknown; + timestamp: number; +}; diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 023aa82c..ecf6fbc1 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -647,10 +647,7 @@ export class VideoConference extends BaseComponent { return participant.id === hostId; }); - if (this.realtime.isLocalParticipantHost) { - this.realtime.setSyncProperty(MeetingEvent.MEETING_HOST_CHANGE, newHost); - this.publish(MeetingEvent.MEETING_HOST_CHANGE, newHost); - } + this.publish(MeetingEvent.MEETING_HOST_CHANGE, newHost); }; /** diff --git a/src/index.ts b/src/index.ts index 54201c15..890f42bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,11 @@ import { WhoIsOnline, } from './components'; import { Transform } from './components/presence-mouse/types'; -import { RealtimeComponentEvent, RealtimeComponentState } from './components/realtime/types'; +import { + RealtimeComponentEvent, + RealtimeComponentState, + RealtimeMessage, +} from './components/realtime/types'; import init from './core'; import './web-components'; import './common/styles/global.css'; @@ -38,7 +42,6 @@ export { Participant, Group, Avatar } from './common/types/participant.types'; export { SuperVizSdkOptions, DevicesOptions } from './common/types/sdk-options.types'; export { BrowserService } from './services/browser'; export { BrowserStats } from './services/browser/types'; -export { RealtimeMessage } from './services/realtime/ably/types'; export { LauncherFacade } from './core/launcher/types'; export { Observer } from './common/utils/observer'; export { @@ -106,6 +109,7 @@ export { WhoIsOnline, VideoConference, Realtime, + RealtimeMessage, }; export default init; diff --git a/src/services/realtime/ably/index.test.ts b/src/services/realtime/ably/index.test.ts index f85e8141..b3301ac1 100644 --- a/src/services/realtime/ably/index.test.ts +++ b/src/services/realtime/ably/index.test.ts @@ -134,13 +134,10 @@ describe('AblyRealtimeService', () => { AblyRealtimeServiceInstance.join(); - expect(AblyRealtimeMock.channels.get).toHaveBeenCalledTimes(5); + expect(AblyRealtimeMock.channels.get).toHaveBeenCalledTimes(4); expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( 'superviz:unit-test-room-id-unit-test-api-key:client-sync', ); - expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( - 'superviz:unit-test-room-id-unit-test-api-key:client-state', - ); expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( 'superviz:unit-test-room-id-unit-test-api-key:broadcast', ); @@ -166,13 +163,10 @@ describe('AblyRealtimeService', () => { const spy = jest.spyOn(AblyRealtimeServiceInstance['broadcastChannel'], 'subscribe'); - expect(AblyRealtimeMock.channels.get).toHaveBeenCalledTimes(5); + expect(AblyRealtimeMock.channels.get).toHaveBeenCalledTimes(4); expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( 'superviz:unit-test-room-id-unit-test-api-key:client-sync', ); - expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( - 'superviz:unit-test-room-id-unit-test-api-key:client-state', - ); expect(AblyRealtimeMock.channels.get).toHaveBeenCalledWith( 'superviz:unit-test-room-id-unit-test-api-key:broadcast', ); diff --git a/src/services/realtime/ably/index.ts b/src/services/realtime/ably/index.ts index 5d45a305..bd32b938 100644 --- a/src/services/realtime/ably/index.ts +++ b/src/services/realtime/ably/index.ts @@ -2,13 +2,10 @@ import Ably from 'ably'; import throttle from 'lodash/throttle'; import { RealtimeEvent, TranscriptState } from '../../../common/types/events.types'; -import { Nullable } from '../../../common/types/global.types'; import { MeetingColors } from '../../../common/types/meeting-colors.types'; import { Participant, ParticipantType } from '../../../common/types/participant.types'; import { RealtimeStateTypes } from '../../../common/types/realtime.types'; import { Annotation } from '../../../components/comments/types'; -import { ParticipantMouse } from '../../../components/presence-mouse/types'; -import { ComponentNames } from '../../../components/types'; import { DrawingData } from '../../video-conference-manager/types'; import { RealtimeService } from '../base'; import { ParticipantInfo, StartRealtimeType } from '../base/types'; @@ -19,31 +16,23 @@ import { AblyRealtimeData, AblyTokenCallBack, ParticipantDataInput, - RealtimeMessage, } from './types'; const MESSAGE_SIZE_LIMIT = 60000; -const CLIENT_MESSAGE_SIZE_LIMIT = 10000; const SYNC_PROPERTY_INTERVAL = 1000; -const SYNC_MOUSE_INTERVAL = 100; export default class AblyRealtimeService extends RealtimeService implements AblyRealtime { private client: Ably.Realtime; private participants: Record = {}; - private participantsWIO: Record = {}; - private participantsMouse: Record = {}; private participantsOn3d: Record = {}; private hostParticipantId: string = null; private myParticipant: AblyParticipant = null; private commentsChannel: Ably.Types.RealtimeChannelCallbacks = null; private supervizChannel: Ably.Types.RealtimeChannelCallbacks = null; private clientSyncChannel: Ably.Types.RealtimeChannelCallbacks = null; - private clientRoomStateChannel: Ably.Types.RealtimeChannelCallbacks = null; private broadcastChannel: Ably.Types.RealtimeChannelCallbacks = null; private presenceWIOChannel: Ably.Types.RealtimeChannelCallbacks = null; private presence3DChannel: Ably.Types.RealtimeChannelCallbacks = null; - private clientRoomState: Record = {}; - private clientSyncPropertiesQueue: Record = {}; private isReconnecting: boolean = false; @@ -195,8 +184,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably // join custom sync channel this.clientSyncChannel = this.client.channels.get(`${this.roomId}:client-sync`); - this.clientRoomStateChannel = this.client.channels.get(`${this.roomId}:client-state`); - this.broadcastChannel = this.client.channels.get(`${this.roomId}:broadcast`); if (!this.enableSync) { this.broadcastChannel.subscribe('update', this.onReceiveBroadcastSync); @@ -302,38 +289,6 @@ export default class AblyRealtimeService extends RealtimeService implements Ably this.updateRoomProperties(Object.assign({}, roomProperties, { transcript: state })); } - /** - * @function setSyncProperty - * @param {string} name - * @param {unknown} property - * @description add/change and sync a property in the room - * @returns {void} - */ - public setSyncProperty(name: string, property: T): void { - // closure to create the event - const createEvent = (name: string, data: T): RealtimeMessage => { - return { - name, - data, - participantId: this.myParticipant.data.participantId, - timestamp: Date.now(), - }; - }; - // if the property is too big, don't add to the queue - if (this.isMessageTooBig(createEvent(name, property), CLIENT_MESSAGE_SIZE_LIMIT)) { - this.logger.log('REALTIME', 'Message too big, not sending'); - this.throw('Message too long, the message limit size is 10kb.'); - } - - this.logger.log('adding to queue', name, property); - - if (!this.clientSyncPropertiesQueue[name]) { - this.clientSyncPropertiesQueue[name] = []; - } - - this.clientSyncPropertiesQueue[name].push(createEvent(name, property)); - } - /** * @function setFollowParticipant * @param {string} participantId diff --git a/src/services/realtime/ably/types.ts b/src/services/realtime/ably/types.ts index e36268a9..b4d0f379 100644 --- a/src/services/realtime/ably/types.ts +++ b/src/services/realtime/ably/types.ts @@ -35,10 +35,3 @@ export type AblyTokenCallBack = ( export interface ParticipantDataInput { [key: string]: string | number | Array | Object; } - -export type RealtimeMessage = { - name: string; - participantId: string; - data: unknown; - timestamp: number; -}; diff --git a/src/services/realtime/base/types.ts b/src/services/realtime/base/types.ts index 601263c8..5944e75b 100644 --- a/src/services/realtime/base/types.ts +++ b/src/services/realtime/base/types.ts @@ -21,7 +21,6 @@ export interface DefaultRealtimeMethods { start: (options: StartRealtimeType) => void; leave: () => void; join: (participant?: Participant) => void; - setSyncProperty: (name: string, property: T) => void; setHost: (masterParticipantId: string) => void; setGridMode: (value: boolean) => void; setDrawing: (drawing: DrawingData) => void; From e5c3aa3cb5edbcd9db8f22eaed8a7ab4ba34b13f Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Mon, 8 Apr 2024 20:17:07 -0300 Subject: [PATCH 80/83] fix: remove flags object, spread properties to options --- src/components/who-is-online/index.ts | 12 ++++++------ src/components/who-is-online/types.ts | 14 ++++++-------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index 71c85278..60fd5480 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -56,12 +56,12 @@ export class WhoIsOnline extends BaseComponent { this.position = options.position ?? Position.TOP_RIGHT; this.setStyles(options.styles); - disablePresenceControls.publish(options.flags?.disablePresenceControls); - disableGoToParticipant.publish(options.flags?.disableGoToParticipant); - disableFollowParticipant.publish(options.flags?.disableFollowParticipant); - disablePrivateMode.publish(options.flags?.disablePrivateMode); - disableGatherAll.publish(options.flags?.disableGatherAll); - disableFollowMe.publish(options.flags?.disableFollowMe); + disablePresenceControls.publish(options.disablePresenceControls); + disableGoToParticipant.publish(options.disableGoToParticipant); + disableFollowParticipant.publish(options.disableFollowParticipant); + disablePrivateMode.publish(options.disablePrivateMode); + disableGatherAll.publish(options.disableGatherAll); + disableFollowMe.publish(options.disableFollowMe); } } diff --git a/src/components/who-is-online/types.ts b/src/components/who-is-online/types.ts index 7a275f51..49bc30a1 100644 --- a/src/components/who-is-online/types.ts +++ b/src/components/who-is-online/types.ts @@ -35,14 +35,12 @@ export type WhoIsOnlinePosition = Position | `${Position}` | string | ''; export interface WhoIsOnlineOptions { position?: WhoIsOnlinePosition; styles?: string; - flags?: { - disablePresenceControls?: boolean; - disableGoToParticipant?: boolean; - disableFollowParticipant?: boolean; - disablePrivateMode?: boolean; - disableGatherAll?: boolean; - disableFollowMe?: boolean; - }; + disablePresenceControls?: boolean; + disableGoToParticipant?: boolean; + disableFollowParticipant?: boolean; + disablePrivateMode?: boolean; + disableGatherAll?: boolean; + disableFollowMe?: boolean; } export enum WIODropdownOptions { From a603f21b937003a1afada0d3b87c6db7e55645d8 Mon Sep 17 00:00:00 2001 From: Ian Silva Date: Tue, 9 Apr 2024 07:22:56 -0300 Subject: [PATCH 81/83] feat: add public signature to transform method --- src/components/presence-mouse/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/presence-mouse/index.ts b/src/components/presence-mouse/index.ts index b7f30c22..2d3efae2 100644 --- a/src/components/presence-mouse/index.ts +++ b/src/components/presence-mouse/index.ts @@ -1,7 +1,7 @@ /* eslint-disable no-constructor-return */ import { PointersCanvas } from './canvas'; import { PointersHTML } from './html'; -import { PresenceMouseProps } from './types'; +import { PresenceMouseProps, Transform } from './types'; export class MousePointers { constructor(containerId: string, options?: PresenceMouseProps) { @@ -14,9 +14,16 @@ export class MousePointers { const tagName = container.tagName.toLowerCase(); if (tagName === 'canvas') { + // @ts-ignore return new PointersCanvas(containerId, options); } + // @ts-ignore return new PointersHTML(containerId, options); } + + public transform(transform: Transform) { + // this is here to give a signature to the method + // the real implementation occurs in each Pointer + } } From 7d1b5ab123c912b06e259253c4e4c99b128a0248 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 9 Apr 2024 08:16:14 -0300 Subject: [PATCH 82/83] fix: any as component type BREAKING CHANGE --- src/core/launcher/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 4daa15a6..f37d71fe 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -79,7 +79,7 @@ export class Launcher extends Observable implements DefaultLauncher { * @param component - component to add * @returns {void} */ - public addComponent = (component: Partial): void => { + public addComponent = (component: any): void => { if (!this.canAddComponent(component)) return; if (!this.realtime.isJoinedRoom) { @@ -133,7 +133,7 @@ export class Launcher extends Observable implements DefaultLauncher { * @param component - component to remove * @returns {void} */ - public removeComponent = (component: Partial): void => { + public removeComponent = (component: any): void => { if (!this.activeComponents.includes(component.name)) { const message = `Component ${component.name} is not initialized yet.`; this.logger.log(message); From ba0858a570773e3473d25d9ec413c2a53bb256e6 Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 9 Apr 2024 08:26:42 -0300 Subject: [PATCH 83/83] feat!: introducing v6 BREAKING CHANGE: v6