From e755e53fed5f067fe531db4adbbfe663b11fbf9f Mon Sep 17 00:00:00 2001 From: jparez Date: Thu, 25 Jul 2024 17:15:31 +0200 Subject: [PATCH 1/4] subscribe to store take an array of dependencies --- src/plugins/KeyboardEvents.js | 17 ++-- src/plugins/KeyboardMapping.js | 17 ++-- src/plugins/MouseEvents.js | 17 ++-- src/plugins/util/OverlayPlugin.js | 17 ++-- src/store/index.js | 124 +++++++++++++++++++----------- 5 files changed, 120 insertions(+), 72 deletions(-) diff --git a/src/plugins/KeyboardEvents.js b/src/plugins/KeyboardEvents.js index 68f3465..45f4d28 100644 --- a/src/plugins/KeyboardEvents.js +++ b/src/plugins/KeyboardEvents.js @@ -58,13 +58,16 @@ module.exports = class KeyboardEvents { this.isListenerAdded = false; this.currentlyPressedKeys = new Map(); - this.instance.store.subscribe(({isKeyboardEventsEnabled}) => { - if (isKeyboardEventsEnabled) { - this.addKeyboardCallbacks(); - } else { - this.removeKeyboardCallbacks(); - } - }); + this.instance.store.subscribe( + ({isKeyboardEventsEnabled}) => { + if (isKeyboardEventsEnabled) { + this.addKeyboardCallbacks(); + } else { + this.removeKeyboardCallbacks(); + } + }, + ['isKeyboardEventsEnabled'], + ); // activate the plugin listening this.instance.store.dispatch({type: 'KEYBOARD_EVENTS_ENABLED', payload: true}); diff --git a/src/plugins/KeyboardMapping.js b/src/plugins/KeyboardMapping.js index 9d2cbf1..52c1c76 100644 --- a/src/plugins/KeyboardMapping.js +++ b/src/plugins/KeyboardMapping.js @@ -157,13 +157,16 @@ module.exports = class KeyboardMapping { ); // pause the listeners when dialog is open and plugin isActive - this.instance.store.subscribe(({overlay: {isOpen}}) => { - if (isOpen && this.state.isActive) { - this.state.isPaused = true; - } else if (!isOpen && this.state.isPaused) { - this.state.isPaused = false; - } - }); + this.instance.store.subscribe( + ({overlay: {isOpen}}) => { + if (isOpen && this.state.isActive) { + this.state.isPaused = true; + } else if (!isOpen && this.state.isPaused) { + this.state.isPaused = false; + } + }, + ['overlay.isOpen'], + ); // Display widget this.renderToolbarButton(); diff --git a/src/plugins/MouseEvents.js b/src/plugins/MouseEvents.js index 4b46727..79cfe78 100644 --- a/src/plugins/MouseEvents.js +++ b/src/plugins/MouseEvents.js @@ -22,13 +22,16 @@ module.exports = class MouseEvents { this.boundEventListener = this.releaseAtPreviousPositionEvent.bind(this); this.removeMouseUpListener = () => {}; - this.instance.store.subscribe(({isMouseEventsEnabled}) => { - if (isMouseEventsEnabled) { - this.addMouseCallbacks(); - } else { - this.removeMouseCallbacks(); - } - }); + this.instance.store.subscribe( + ({isMouseEventsEnabled}) => { + if (isMouseEventsEnabled) { + this.addMouseCallbacks(); + } else { + this.removeMouseCallbacks(); + } + }, + ['isMouseEventsEnabled'], + ); this.mouseCallbacks = []; diff --git a/src/plugins/util/OverlayPlugin.js b/src/plugins/util/OverlayPlugin.js index 601a369..4c0b5e8 100644 --- a/src/plugins/util/OverlayPlugin.js +++ b/src/plugins/util/OverlayPlugin.js @@ -19,13 +19,16 @@ class OverlayPlugin { this.instance = instance; // Listen for close trigger - this.instance.store.subscribe(({overlay}) => { - if (overlay.widgetOpened.includes(this.overlayID)) { - this.openOverlay(); - } else { - this.closeOverlay(); - } - }); + this.instance.store.subscribe( + ({overlay}) => { + if (overlay.widgetOpened.includes(this.overlayID)) { + this.openOverlay(); + } else { + this.closeOverlay(); + } + }, + ['overlay.widgetOpened'], + ); // Attach listener for first object created only if (!OverlayPlugin.hasBeenCalled) { diff --git a/src/store/index.js b/src/store/index.js index ca59634..921b1bb 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -13,7 +13,7 @@ const initialState = { isMouseEventsEnabled: false, }; -const createStore = (instance, reducer, state) => { +const createStore = (instance, reducer) => { const listeners = []; const getters = { @@ -21,18 +21,71 @@ const createStore = (instance, reducer, state) => { instance.store.state.overlay.isOpen && instance.store.state.overlay.widgetOpened.includes(overlayID), }; - const dispatch = (action) => { - instance.store.state = reducer(instance.store.state, action); - listeners.forEach(({cb}) => { - cb(); + const hasChanged = (changedKeys, keyPath) => { + const keys = keyPath.split('.'); + return changedKeys.some((changedKey) => { + const changedKeyParts = changedKey.split('.'); + return keys.every((key, index) => key === changedKeyParts[index]); + }); + }; + + const findChangedKeys = (newState, oldState, path = []) => { + let changedKeys = []; + + const isObject = (obj) => obj && typeof obj === 'object'; + + for (const key in newState) { + const fullPath = [...path, key].join('.'); + + if (!Object.prototype.hasOwnProperty.call(oldState, key)) { + changedKeys.push(fullPath); + } else if (isObject(newState[key]) && isObject(oldState[key])) { + if (Array.isArray(newState[key]) && Array.isArray(oldState[key])) { + if (newState[key].length !== oldState[key].length) { + changedKeys.push(fullPath); + } else { + let arrayHasChanged = false; + for (let i = 0; i < newState[key].length; i++) { + if (newState[key][i] !== oldState[key][i]) { + arrayHasChanged = true; + } + } + if (arrayHasChanged) { + changedKeys.push(fullPath); + } + } + } else { + changedKeys = changedKeys.concat(findChangedKeys(newState[key], oldState[key], [...path, key])); + } + } else if (newState[key] !== oldState[key]) { + changedKeys.push(fullPath); + } + } + + return changedKeys; + }; + + const notifyListeners = (changedKeys) => { + listeners.forEach(({keys, cb}) => { + if (keys.length === 0 || keys.some((key) => hasChanged(changedKeys, key))) { + cb(instance.store.state); + } }); }; - const subscribe = (listener) => { + const dispatch = (action) => { + const previousState = JSON.parse(JSON.stringify(instance.store.state)); + instance.store.state = reducer({...instance.store.state}, action); + const changedKeys = findChangedKeys(instance.store.state, previousState); + notifyListeners(changedKeys); + }; + + const subscribe = (listener, keys = []) => { const uid = generateUID(); listeners.push({ uid, - cb: () => listener(instance.store.state), + keys, + cb: listener, }); const unsubscribe = () => { @@ -45,68 +98,51 @@ const createStore = (instance, reducer, state) => { return unsubscribe; }; - instance.store = {state, dispatch, subscribe, getters}; + instance.store = {state: initialState, dispatch, subscribe, getters}; }; const reducer = (state, action) => { - let newState = state; switch (action.type) { case 'WEBRTC_CONNECTION_READY': - newState = {...state, isWebRTCConnectionReady: action.payload}; + state.isWebRTCConnectionReady = action.payload; break; case 'KEYBOARD_EVENTS_ENABLED': - newState = {...state, isKeyboardEventsEnabled: action.payload}; + state.isKeyboardEventsEnabled = action.payload; break; case 'MOUSE_EVENTS_ENABLED': - newState = {...state, isMouseEventsEnabled: action.payload}; + state.isMouseEventsEnabled = action.payload; break; case 'OVERLAY_OPEN': // eslint-disable-next-line no-case-declarations const {overlayID, toOpen} = action.payload; if (toOpen) { - newState = { - ...state, - overlay: { - isOpen: true, - widgetOpened: [overlayID], - /* - * to open several widgets at the same time - * widgetOpened: [...state.overlay.widgetOpened, overlayID], - */ - }, - isKeyboardEventsEnabled: false, - isMouseEventsEnabled: false, - }; - break; + state.overlay.isOpen = true; + /* + * to open several widgets at the same time + * widgetOpened: [...state.overlay.widgetOpened, overlayID], + */ + state.overlay.widgetOpened = [overlayID]; + state.isKeyboardEventsEnabled = false; + state.isMouseEventsEnabled = false; + } else { + state.overlay.isOpen = false; + state.overlay.widgetOpened = []; + state.isKeyboardEventsEnabled = true; + state.isMouseEventsEnabled = true; } - // eslint-disable-next-line no-case-declarations - const widgetOpened = []; - /* - * to open several widgets at the same time - * const widgetOpened = state.overlay.widgetOpened.filter((widgetId) => widgetId !== overlayID); - */ - newState = { - ...state, - overlay: { - isOpen: widgetOpened.length > 0, - widgetOpened, - }, - isKeyboardEventsEnabled: true, - isMouseEventsEnabled: true, - }; break; default: log.debug('Store not updated, action type :', action.type, ' unknown'); break; } - log.debug('Store updated below type - payload - result', action.type, action.payload, newState); + log.debug('Store updated below type - payload - result', action.type, action.payload, state); - return newState; + return state; }; const store = (instance) => { - createStore(instance, reducer, initialState); + createStore(instance, reducer); }; module.exports = store; From e1487ef011bbb61b990d91601c512f0ffb4a8409 Mon Sep 17 00:00:00 2001 From: jparez Date: Thu, 25 Jul 2024 17:15:31 +0200 Subject: [PATCH 2/4] adding feature to trackEvents from outside of webplayer --- src/plugins/FileUpload.js | 7 ++++ src/plugins/GamepadManager.js | 14 ++++++++ src/plugins/util/OverlayPlugin.js | 8 +++++ src/store/index.js | 55 ++++++++++++++++++----------- tests/unit/apiManager.test.js | 57 +++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 tests/unit/apiManager.test.js diff --git a/src/plugins/FileUpload.js b/src/plugins/FileUpload.js index 4f773d0..bc455ac 100644 --- a/src/plugins/FileUpload.js +++ b/src/plugins/FileUpload.js @@ -277,6 +277,13 @@ module.exports = class FileUpload extends OverlayPlugin { textProgress.innerHTML = this.i18n.UPLOADER_INSTALLING || 'Installing...'; } else if (data[0] === 'ready' && data.length >= 2) { if (data[1].indexOf('opengapps') !== -1) { + this.instance.store.dispatch({ + type: 'ADD_TRACKED_EVENT', + payload: { + category: 'opengapps', + type: 'installed', + }, + }); this.updateOpenGAppsStatus(true); } diff --git a/src/plugins/GamepadManager.js b/src/plugins/GamepadManager.js index ef23562..15790a0 100644 --- a/src/plugins/GamepadManager.js +++ b/src/plugins/GamepadManager.js @@ -262,6 +262,13 @@ module.exports = class GamepadManager { * @param {GamepadEvent} event raw event coming from the browser Gamepad API */ onGamepadConnected(event) { + this.instance.store.dispatch({ + type: 'ADD_TRACKED_EVENT', + payload: { + category: 'gamepad', + action: 'plugged', + }, + }); const customEvent = new CustomEvent('gm-gamepadConnected', {detail: this.parseGamepad(event.gamepad)}); window.dispatchEvent(customEvent); } @@ -271,6 +278,13 @@ module.exports = class GamepadManager { * @param {GamepadEvent} event raw event coming from the browser Gamepad API */ onGamepadDisconnected(event) { + this.instance.store.dispatch({ + type: 'ADD_TRACKED_EVENT', + payload: { + category: 'gamepad', + action: 'unplugged', + }, + }); const customEvent = new CustomEvent('gm-gamepadDisconnected', {detail: this.parseGamepad(event.gamepad)}); window.dispatchEvent(customEvent); this.stopListeningInputs(event.gamepad.index); diff --git a/src/plugins/util/OverlayPlugin.js b/src/plugins/util/OverlayPlugin.js index 4c0b5e8..ea7cabb 100644 --- a/src/plugins/util/OverlayPlugin.js +++ b/src/plugins/util/OverlayPlugin.js @@ -61,6 +61,14 @@ class OverlayPlugin { if (this.toolbarBtnImage) { this.toolbarBtnImage.classList.add('gm-active'); } + this.instance.store.dispatch({ + type: 'ADD_TRACKED_EVENT', + payload: { + category: 'widget', + action: 'open', + name: this.constructor.name, + }, + }); } /** diff --git a/src/store/index.js b/src/store/index.js index 921b1bb..41b2d07 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -11,6 +11,10 @@ const initialState = { }, isKeyboardEventsEnabled: false, isMouseEventsEnabled: false, + trackedEvents: { + isActive: false, + events: [], + }, }; const createStore = (instance, reducer) => { @@ -29,31 +33,31 @@ const createStore = (instance, reducer) => { }); }; + const arraysAreEqual = (firstArray, secondArray) => { + if (firstArray.length !== secondArray.length) { + return true; + } + for (let i = 0; i < firstArray.length; i++) { + if (firstArray[i] !== secondArray[i]) { + return true; + } + } + return false; + }; const findChangedKeys = (newState, oldState, path = []) => { let changedKeys = []; - const isObject = (obj) => obj && typeof obj === 'object'; - for (const key in newState) { const fullPath = [...path, key].join('.'); - if (!Object.prototype.hasOwnProperty.call(oldState, key)) { changedKeys.push(fullPath); } else if (isObject(newState[key]) && isObject(oldState[key])) { - if (Array.isArray(newState[key]) && Array.isArray(oldState[key])) { - if (newState[key].length !== oldState[key].length) { - changedKeys.push(fullPath); - } else { - let arrayHasChanged = false; - for (let i = 0; i < newState[key].length; i++) { - if (newState[key][i] !== oldState[key][i]) { - arrayHasChanged = true; - } - } - if (arrayHasChanged) { - changedKeys.push(fullPath); - } - } + if ( + Array.isArray(newState[key]) && + Array.isArray(oldState[key]) && + !arraysAreEqual(newState[key], oldState[key]) + ) { + changedKeys.push(fullPath); } else { changedKeys = changedKeys.concat(findChangedKeys(newState[key], oldState[key], [...path, key])); } @@ -61,14 +65,14 @@ const createStore = (instance, reducer) => { changedKeys.push(fullPath); } } - return changedKeys; }; const notifyListeners = (changedKeys) => { listeners.forEach(({keys, cb}) => { if (keys.length === 0 || keys.some((key) => hasChanged(changedKeys, key))) { - cb(instance.store.state); + // send a copy of the store's state, in order to avoid mutation of the store + cb({...instance.store.state}); } }); }; @@ -131,6 +135,19 @@ const reducer = (state, action) => { state.isMouseEventsEnabled = true; } break; + case 'ENABLE_TRACKED_EVENTS': + state.trackedEvents.isActive = action.payload; + break; + case 'ADD_TRACKED_EVENT': + if (!state.trackedEvents.isActive) { + return state; + } + state.trackedEvents.events.push(action.payload); + + break; + case 'FLUSH_TRACKED_EVENTS': + state.trackedEvents.events.length = 0; + break; default: log.debug('Store not updated, action type :', action.type, ' unknown'); break; diff --git a/tests/unit/apiManager.test.js b/tests/unit/apiManager.test.js new file mode 100644 index 0000000..d2084c7 --- /dev/null +++ b/tests/unit/apiManager.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const ApiManager = require('../../src/APIManager'); +const Instance = require('../mocks/DeviceRenderer'); +const Clipboard = require('../../src/plugins/Clipboard'); +const store = require('../../src/store/index'); + +let apiManager; +let instance; +let exposedApiFunctions; + +describe('APIManager', () => { + beforeEach(() => { + instance = new Instance({}); + apiManager = new ApiManager(instance); + exposedApiFunctions = apiManager.getExposedApiFunctions(); + store(instance); + + new Clipboard(instance, { + CLIPBOARD_TITLE: 'TEST CLIPBOARD PLUGIN TITLE', + }); + }); + + describe('has exposed api', () => { + test('getRegisteredFunctions', () => { + const registeredFunctions = exposedApiFunctions.utils.getRegisteredFunctions(); + expect(Object.keys(registeredFunctions)).toEqual( + expect.arrayContaining([ + 'sendData', + 'getRegisteredFunctions', + 'addEventListener', + 'disconnect', + 'enableTrackEvents', + 'trackEvents', + ]), + ); + }); + + test('sendTrackEvent', () => { + let events = []; + + exposedApiFunctions.analytics.enableTrackEvents(true); + + // attach callback to get events + exposedApiFunctions.analytics.trackEvents((evts) => { + events = evts; + }); + + const button = document.getElementsByClassName('gm-clipboard-button')[0]; + expect(button).toBeTruthy(); + button.click(); + + // expect object to be exactly the same + expect(events).toEqual([{category: 'widget', action: 'open', name: 'Clipboard'}]); + }); + }); +}); From e099e07616583e11cf016d567789021aa512f5dc Mon Sep 17 00:00:00 2001 From: jparez Date: Fri, 26 Jul 2024 14:26:00 +0200 Subject: [PATCH 3/4] fix issue when toolbar not loaded and keymapping being activate --- src/plugins/KeyboardMapping.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/KeyboardMapping.js b/src/plugins/KeyboardMapping.js index 52c1c76..f11e543 100644 --- a/src/plugins/KeyboardMapping.js +++ b/src/plugins/KeyboardMapping.js @@ -193,12 +193,16 @@ module.exports = class KeyboardMapping { activatePlugin() { if (this.state.isActive) { - this.toolbarBtnImage.classList.add('gm-active'); + if (this.toolbarBtnImage) { + this.toolbarBtnImage.classList.add('gm-active'); + } this.addKeyboardCallbacks(); this.instance.store.dispatch({type: 'KEYBOARD_EVENTS_ENABLED', payload: false}); this.instance.store.dispatch({type: 'MOUSE_EVENTS_ENABLED', payload: false}); } else { - this.toolbarBtnImage.classList.remove('gm-active'); + if (this.toolbarBtnImage) { + this.toolbarBtnImage.classList.remove('gm-active'); + } this.removeKeyboardCallbacks(); this.instance.store.dispatch({type: 'KEYBOARD_EVENTS_ENABLED', payload: !this.state.isPaused}); this.instance.store.dispatch({type: 'MOUSE_EVENTS_ENABLED', payload: !this.state.isPaused}); From 62e7c7921e50d9cb632bcf0ee2e798d4088a4022 Mon Sep 17 00:00:00 2001 From: jparez Date: Fri, 26 Jul 2024 14:57:59 +0200 Subject: [PATCH 4/4] cancel rewrite findchangeKeys --- src/store/index.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index 41b2d07..0f88529 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -33,31 +33,31 @@ const createStore = (instance, reducer) => { }); }; - const arraysAreEqual = (firstArray, secondArray) => { - if (firstArray.length !== secondArray.length) { - return true; - } - for (let i = 0; i < firstArray.length; i++) { - if (firstArray[i] !== secondArray[i]) { - return true; - } - } - return false; - }; const findChangedKeys = (newState, oldState, path = []) => { let changedKeys = []; + const isObject = (obj) => obj && typeof obj === 'object'; + for (const key in newState) { const fullPath = [...path, key].join('.'); + if (!Object.prototype.hasOwnProperty.call(oldState, key)) { changedKeys.push(fullPath); } else if (isObject(newState[key]) && isObject(oldState[key])) { - if ( - Array.isArray(newState[key]) && - Array.isArray(oldState[key]) && - !arraysAreEqual(newState[key], oldState[key]) - ) { - changedKeys.push(fullPath); + if (Array.isArray(newState[key]) && Array.isArray(oldState[key])) { + if (newState[key].length !== oldState[key].length) { + changedKeys.push(fullPath); + } else { + let arrayHasChanged = false; + for (let i = 0; i < newState[key].length; i++) { + if (newState[key][i] !== oldState[key][i]) { + arrayHasChanged = true; + } + } + if (arrayHasChanged) { + changedKeys.push(fullPath); + } + } } else { changedKeys = changedKeys.concat(findChangedKeys(newState[key], oldState[key], [...path, key])); } @@ -65,6 +65,7 @@ const createStore = (instance, reducer) => { changedKeys.push(fullPath); } } + return changedKeys; };