Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLAYER-25] adding api to track event #84

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/plugins/FileUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
14 changes: 14 additions & 0 deletions src/plugins/GamepadManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
17 changes: 10 additions & 7 deletions src/plugins/KeyboardEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
25 changes: 16 additions & 9 deletions src/plugins/KeyboardMapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -190,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});
Expand Down
17 changes: 10 additions & 7 deletions src/plugins/MouseEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down
25 changes: 18 additions & 7 deletions src/plugins/util/OverlayPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
);
pgivel marked this conversation as resolved.
Show resolved Hide resolved

// Attach listener for first object created only
if (!OverlayPlugin.hasBeenCalled) {
Expand Down Expand Up @@ -58,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,
},
});
}

/**
Expand Down
142 changes: 98 additions & 44 deletions src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,86 @@ const initialState = {
},
isKeyboardEventsEnabled: false,
isMouseEventsEnabled: false,
trackedEvents: {
isActive: false,
events: [],
},
};

const createStore = (instance, reducer, state) => {
const createStore = (instance, reducer) => {
const listeners = [];

const getters = {
isWidgetOpened: (overlayID) =>
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;
};
Comment on lines +36 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice for readibility to refactor this function in order to avoid such deep nesting of conditions & loops. For example by extracting to a function the array check ?

Suggested change
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 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);
} else {
changedKeys = changedKeys.concat(findChangedKeys(newState[key], oldState[key], [...path, key]));
}
} else if (newState[key] !== oldState[key]) {
changedKeys.push(fullPath);
}
}
return changedKeys;
};

But there's probably ways to reduce even more the nesting.


const notifyListeners = (changedKeys) => {
listeners.forEach(({keys, cb}) => {
if (keys.length === 0 || keys.some((key) => hasChanged(changedKeys, key))) {
// send a copy of the store's state, in order to avoid mutation of the store
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 = () => {
Expand All @@ -45,68 +103,64 @@ 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;
case 'ENABLE_TRACKED_EVENTS':
state.trackedEvents.isActive = action.payload;
break;
case 'ADD_TRACKED_EVENT':
if (!state.trackedEvents.isActive) {
return state;
pgivel marked this conversation as resolved.
Show resolved Hide resolved
}
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;
}

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;
Loading
Loading