diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 6fa3a46dca..7478ca9c2b 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -346,7 +346,12 @@ Item { maximumValue: 20.0 stepSize: 5 updateValueWhileDragging: true - onValueChanged: updateGainFromQML(uuid, value) + onValueChanged: updateGainFromQML(uuid, value, false) + onPressedChanged: { + if (!pressed) { + updateGainFromQML(uuid, value, true) + } + } MouseArea { anchors.fill: parent onWheel: { @@ -360,7 +365,8 @@ Item { mouse.accepted = false } onReleased: { - // Pass through to Slider + // the above mouse.accepted seems to make this + // never get called, nonetheless... mouse.accepted = false } } @@ -382,12 +388,13 @@ Item { } } - function updateGainFromQML(avatarUuid, sliderValue) { - if (pal.gainSliderValueDB[avatarUuid] !== sliderValue) { + function updateGainFromQML(avatarUuid, sliderValue, isReleased) { + if (isReleased || pal.gainSliderValueDB[avatarUuid] !== sliderValue) { pal.gainSliderValueDB[avatarUuid] = sliderValue; var data = { sessionId: avatarUuid, - gain: sliderValue + gain: sliderValue, + isReleased: isReleased }; pal.sendToScript({method: 'updateGain', params: data}); } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 09ae5223e7..989b560e4e 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -250,6 +250,7 @@ Rectangle { userModel.setProperty(model.userIndex, styleData.role, newValue) userModelData[model.userIndex][styleData.role] = newValue // Defensive programming Users[styleData.role](model.sessionId, newValue) + UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId) if (styleData.role === "ignore") { userModel.setProperty(model.userIndex, "personalMute", newValue) userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming @@ -276,6 +277,7 @@ Rectangle { height: 24 onClicked: { Users[styleData.role](model.sessionId) + UserActivityLogger["palAction"](styleData.role, model.sessionId) if (styleData.role === "kick") { // Just for now, while we cannot undo "Ban": userModel.remove(model.userIndex) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8abf63f9f2..3d109a6549 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1937,6 +1937,8 @@ void Application::initializeUi() { rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); rootContext->setContextProperty("Users", DependencyManager::get().data()); + rootContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + rootContext->setContextProperty("Camera", &_myCamera); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index fa8cd9abd9..02d1711230 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -38,6 +38,21 @@ void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, i } +void UserActivityLoggerScriptingInterface::palAction(QString action, QString target) { + QJsonObject payload; + payload["action"] = action; + if (target.length() > 0) { + payload["target"] = target; + } + logAction("pal_activity", payload); +} + +void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) { + logAction("pal_opened", { + { "seconds_opened", secondsOpened } + }); +} + void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", Q_ARG(QString, action), diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 07459967bc..a202858a1c 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -25,7 +25,8 @@ public: Q_INVOKABLE void toggledAway(bool isAway); Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = ""); - + Q_INVOKABLE void palAction(QString action, QString target); + Q_INVOKABLE void palOpened(float secondsOpen); private: void logAction(QString action, QJsonObject details = {}); }; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index cbfac76545..3b34eed268 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -103,6 +103,8 @@ ExtendedOverlay.prototype.select = function (selected) { return; } + UserActivityLogger.palAction(selected ? "avatar_selected" : "avatar_deselected", this.key); + this.editOverlay({color: color(selected, this.hovering, this.audioLevel)}); if (this.model) { this.model.editOverlay({textures: textures(selected)}); @@ -232,14 +234,24 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like case 'refresh': removeOverlays(); populateUserList(); + UserActivityLogger.palAction("refresh", ""); break; case 'updateGain': data = message.params; - Users.setAvatarGain(data['sessionId'], data['gain']); + if (data['isReleased']) { + // isReleased=true happens once at the end of a cycle of dragging + // the slider about, but with same gain as last isReleased=false so + // we don't set the gain in that case, and only here do we want to + // send an analytic event. + UserActivityLogger.palAction("avatar_gain_changed", data['sessionId']); + } else { + Users.setAvatarGain(data['sessionId'], data['gain']); + } break; case 'displayNameUpdate': if (MyAvatar.displayName != message.params) { MyAvatar.displayName = message.params; + UserActivityLogger.palAction("display_name_change", ""); } break; default: @@ -552,7 +564,10 @@ var button = toolBar.addButton({ buttonState: 1, alpha: 0.9 }); + var isWired = false; +var palOpenedAt; + function off() { if (isWired) { // It is not ok to disconnect these twice, hence guard. Script.update.disconnect(updateOverlays); @@ -564,6 +579,11 @@ function off() { triggerPressMapping.disable(); // see above removeOverlays(); Users.requestsDomainListData = false; + if (palOpenedAt) { + var duration = new Date().getTime() - palOpenedAt; + UserActivityLogger.palOpened(duration / 1000.0); + palOpenedAt = 0; // just a falsy number is good enough. + } if (audioInterval) { Script.clearInterval(audioInterval); } @@ -580,6 +600,7 @@ function onClicked() { triggerMapping.enable(); triggerPressMapping.enable(); createAudioInterval(); + palOpenedAt = new Date().getTime(); } else { off(); }