diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index c6bcc5af34..5e1d747604 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -782,41 +782,48 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer message, SharedNodePointer sendingNode) { // From the packet, pull the UUID we're identifying QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + if (!nodeUUID.isNull()) { - // Before we do any processing on this packet, make sure it comes from a node that is allowed to kick (is an admin) - // OR from a node whose UUID matches the one in the packet - if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) { - // First, make sure we actually have a node with this UUID - auto limitedNodeList = DependencyManager::get(); - auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); + // First, make sure we actually have a node with this UUID + auto limitedNodeList = DependencyManager::get(); + auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); - // If we do have a matching node... - if (matchingNode) { + // If we do have a matching node... + if (matchingNode) { + // Setup the packet + auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply); + + QString verifiedUsername; + QUuid machineFingerprint; + + // Write the UUID to the packet + usernameFromIDReplyPacket->write(nodeUUID.toRfc4122()); + + // Check if the sending node has permission to kick (is an admin) + // OR if the message is from a node whose UUID matches the one in the packet + if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) { // It's time to figure out the username - QString verifiedUsername = matchingNode->getPermissions().getVerifiedUserName(); - - // Setup the packet - auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply); - usernameFromIDReplyPacket->write(nodeUUID.toRfc4122()); + verifiedUsername = matchingNode->getPermissions().getVerifiedUserName(); usernameFromIDReplyPacket->writeString(verifiedUsername); // now put in the machine fingerprint DomainServerNodeData* nodeData = reinterpret_cast(matchingNode->getLinkedData()); - QUuid machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid(); + machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid(); usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122()); - - qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID; - - // Ship it! - limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode); } else { - qWarning() << "Node username request received for unknown node. Refusing to process."; + usernameFromIDReplyPacket->writeString(verifiedUsername); + usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122()); } - } else { - qWarning() << "Refusing to process a username request packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()) - << ". Either node doesn't have kick permissions or is requesting a username not from their UUID."; - } + // Write whether or not the user is an admin + bool isAdmin = matchingNode->getCanKick(); + usernameFromIDReplyPacket->writePrimitive(isAdmin); + qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID << ". Node admin status: " << isAdmin; + // Ship it! + limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode); + } else { + qWarning() << "Node username request received for unknown node. Refusing to process."; + } } else { qWarning() << "Node username request received for invalid node ID. Refusing to process."; } diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index c8adcdcb74..e50d1de547 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -15,7 +15,13 @@ import "../styles-uit" Item { property alias text: popupText.text + property alias headerGlyph: headerGlyph.text + property alias headerText: headerText.text property real popupRadius: hifi.dimensions.borderRadius + property real headerTextPixelSize: 22 + property real popupTextPixelSize: 16 + FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; } visible: false id: letterbox anchors.fill: parent @@ -27,19 +33,79 @@ Item { } Rectangle { width: Math.max(parent.width * 0.75, 400) - height: popupText.contentHeight*1.5 + height: contentContainer.height + 50 anchors.centerIn: parent radius: popupRadius color: "white" - FiraSansSemiBold { - id: popupText - size: hifi.fontSizes.textFieldInput - color: hifi.colors.darkGray - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - anchors.leftMargin: 15 - anchors.rightMargin: 15 - wrapMode: Text.WordWrap + Item { + id: contentContainer + width: parent.width - 60 + height: childrenRect.height + anchors.centerIn: parent + Item { + id: popupHeaderContainer + visible: headerText.text !== "" || headerGlyph.text !== "" + height: 30 + // Anchors + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + // Header Glyph + HiFiGlyphs { + id: headerGlyph + visible: headerGlyph.text !== "" + // Size + height: parent.height + // Anchors + anchors.left: parent.left + anchors.leftMargin: -15 + // Text Size + size: headerTextPixelSize*2.5 + // Style + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + color: hifi.colors.darkGray + } + // Header Text + Text { + id: headerText + visible: headerText.text !== "" + // Size + height: parent.height + // Anchors + anchors.left: headerGlyph.right + anchors.leftMargin: -5 + // Text Size + font.pixelSize: headerTextPixelSize + // Style + font.family: ralewaySemiBold.name + color: hifi.colors.darkGray + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + textFormat: Text.StyledText + } + } + // Popup Text + Text { + id: popupText + // Size + width: parent.width + // Anchors + anchors.top: popupHeaderContainer.visible ? popupHeaderContainer.bottom : parent.top + anchors.topMargin: popupHeaderContainer.visible ? 15 : 0 + anchors.left: parent.left + anchors.right: parent.right + // Text alignment + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHLeft + // Style + font.pixelSize: popupTextPixelSize + font.family: ralewayRegular.name + color: hifi.colors.darkGray + wrapMode: Text.WordWrap + textFormat: Text.StyledText + } } } MouseArea { diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 2725ea1617..7478ca9c2b 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -33,6 +33,7 @@ Item { property real audioLevel: 0.0 property bool isMyCard: false property bool selected: false + property bool isAdmin: false /* User image commented out for now - will probably be re-introduced later. Column { @@ -139,32 +140,94 @@ Item { } } // Spacer for DisplayName for my card - Rectangle { + Item { id: myDisplayNameSpacer - width: myDisplayName.width + width: 1 + height: 4 // Anchors anchors.top: myDisplayName.bottom - height: 5 - visible: isMyCard - opacity: 0 } - // DisplayName Text for others' cards - FiraSansSemiBold { - id: displayNameText - // Properties - text: thisNameCard.displayName - elide: Text.ElideRight + // DisplayName container for others' cards + Item { + id: displayNameContainer visible: !isMyCard // Size width: parent.width + height: displayNameTextPixelSize + 4 // Anchors anchors.top: parent.top - // Text Size - size: displayNameTextPixelSize - // Text Positioning - verticalAlignment: Text.AlignVCenter - // Style - color: hifi.colors.darkGray + anchors.left: parent.left + // DisplayName Text for others' cards + FiraSansSemiBold { + id: displayNameText + // Properties + text: thisNameCard.displayName + elide: Text.ElideRight + // Size + width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width + // Anchors + anchors.top: parent.top + anchors.left: parent.left + // Text Size + size: displayNameTextPixelSize + // Text Positioning + verticalAlignment: Text.AlignVCenter + // Style + color: hifi.colors.darkGray + } + TextMetrics { + id: displayNameTextMetrics + font: displayNameText.font + text: displayNameText.text + } + // "ADMIN" label for other users' cards + RalewaySemiBold { + id: adminLabelText + visible: isAdmin + text: "ADMIN" + // Text size + size: displayNameText.size - 4 + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: displayNameText.right + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Item { + id: adminLabelQuestionMark + visible: isAdmin + // Size + width: 20 + height: displayNameText.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: adminLabelText.right + RalewayRegular { + id: adminLabelQuestionMarkText + text: "[?]" + size: adminLabelText.size + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: letterbox(hifi.glyphs.question, + "Domain Admin", + "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") + onEntered: adminLabelQuestionMarkText.color = "#94132e" + onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight + } + } } // UserName Text @@ -177,7 +240,7 @@ Item { // Size width: parent.width // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameText.bottom + anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom // Text Size size: thisNameCard.usernameTextHeight // Text Positioning @@ -188,7 +251,7 @@ Item { // Spacer Item { - id: spacer + id: userNameSpacer height: 4 width: parent.width // Anchors @@ -199,10 +262,10 @@ Item { Rectangle { id: nameCardVUMeter // Size - width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width + width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width height: 8 // Anchors - anchors.top: spacer.bottom + anchors.top: userNameSpacer.bottom // Style radius: 4 color: "#c5c5c5" @@ -283,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: { @@ -297,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 } } @@ -319,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 7260cd1c14..989b560e4e 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -28,7 +28,7 @@ Rectangle { property int rowHeight: 70 property int actionButtonWidth: 75 property int nameCardWidth: palContainer.width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth - property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set + property var myData: ({displayName: "", userName: "", audioLevel: 0.0, admin: true}) // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. property bool iAmAdmin: false @@ -41,7 +41,9 @@ Rectangle { id: letterboxMessage z: 999 // Force the popup on top of everything else } - function letterbox(message) { + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph + letterboxMessage.headerText = headerText letterboxMessage.text = message letterboxMessage.visible = true letterboxMessage.popupRadius = 0 @@ -221,6 +223,7 @@ Rectangle { visible: !isCheckBox && !isButton uuid: model && model.sessionId selected: styleData.selected + isAdmin: model && model.admin // Size width: nameCardWidth height: parent.height @@ -247,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 @@ -273,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) @@ -369,9 +374,11 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true - onClicked: letterbox("Bold names in the list are Avatar Display Names.\n" + - "If a Display Name isn't set, a unique Session Display Name is assigned." + - "\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present.") + onClicked: letterbox(hifi.glyphs.question, + "Display Names", + "Bold names in the list are avatar display names.
" + + "If a display name isn't set, a unique session display name is assigned." + + "

Administrators of this domain can also see the username or machine ID associated with each avatar present.") onEntered: helpText.color = hifi.colors.baseGrayHighlight onExited: helpText.color = hifi.colors.darkGray } @@ -400,8 +407,10 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true - onClicked: letterbox('Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' + - "Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page.)") + onClicked: letterbox(hifi.glyphs.question, + "Admin Actions", + "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + + "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.") onEntered: adminHelpText.color = "#94132e" onExited: adminHelpText.color = hifi.colors.redHighlight } @@ -446,9 +455,9 @@ Rectangle { var selected = message.params[1]; var userIndex = findSessionIndex(sessionIds[0]); if (sessionIds.length > 1) { - letterbox('Only one user can be selected at a time.'); + letterbox("", "", 'Only one user can be selected at a time.'); } else if (userIndex < 0) { - letterbox('The last editor is not among this list of users.'); + letterbox("", "", 'The last editor is not among this list of users.'); } else { if (selected) { table.selection.clear(); // for now, no multi-select @@ -465,6 +474,7 @@ Rectangle { var userId = message.params[0]; // The text that goes in the userName field is the second parameter in the message. var userName = message.params[1]; + var admin = message.params[2]; // If the userId is empty, we're updating "myData". if (!userId) { myData.userName = userName; @@ -476,6 +486,9 @@ Rectangle { // Set the userName appropriately userModel.setProperty(userIndex, "userName", userName); userModelData[userIndex].userName = userName; // Defensive programming + // Set the admin status appropriately + userModel.setProperty(userIndex, "admin", admin); + userModelData[userIndex].admin = admin; // Defensive programming } } break; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cd6f6f8dc4..f6bcf591fc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1941,6 +1941,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/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 0fcd207b94..7bfe1d1845 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -1040,25 +1040,20 @@ void NodeList::muteNodeBySessionID(const QUuid& nodeID) { } void NodeList::requestUsernameFromSessionID(const QUuid& nodeID) { - // send a request to domain-server to get the username associated with the given session ID - if (getThisNodeCanKick() || nodeID.isNull()) { - // setup the packet - auto usernameFromIDRequestPacket = NLPacket::create(PacketType::UsernameFromIDRequest, NUM_BYTES_RFC4122_UUID, true); + // send a request to domain-server to get the username/machine fingerprint/admin status associated with the given session ID + // If the requesting user isn't an admin, the username and machine fingerprint will return "". + auto usernameFromIDRequestPacket = NLPacket::create(PacketType::UsernameFromIDRequest, NUM_BYTES_RFC4122_UUID, true); - // write the node ID to the packet - if (nodeID.isNull()) { - usernameFromIDRequestPacket->write(getSessionUUID().toRfc4122()); - } else { - usernameFromIDRequestPacket->write(nodeID.toRfc4122()); - } - - qCDebug(networking) << "Sending packet to get username of node" << uuidStringWithoutCurlyBraces(nodeID); - - sendPacket(std::move(usernameFromIDRequestPacket), _domainHandler.getSockAddr()); + // write the node ID to the packet + if (nodeID.isNull()) { + usernameFromIDRequestPacket->write(getSessionUUID().toRfc4122()); } else { - qWarning() << "You do not have permissions to kick in this domain." - << "Request to get the username of node" << uuidStringWithoutCurlyBraces(nodeID) << "will not be sent"; + usernameFromIDRequestPacket->write(nodeID.toRfc4122()); } + + qCDebug(networking) << "Sending packet to get username/fingerprint/admin status of node" << uuidStringWithoutCurlyBraces(nodeID); + + sendPacket(std::move(usernameFromIDRequestPacket), _domainHandler.getSockAddr()); } void NodeList::processUsernameFromIDReply(QSharedPointer message) { @@ -1068,10 +1063,13 @@ void NodeList::processUsernameFromIDReply(QSharedPointer messag QString username = message->readString(); // read the machine fingerprint from the packet QString machineFingerprintString = (QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID))).toString(); + bool isAdmin; + message->readPrimitive(&isAdmin); - qCDebug(networking) << "Got username" << username << "and machine fingerprint" << machineFingerprintString << "for node" << nodeUUIDString; + qCDebug(networking) << "Got username" << username << "and machine fingerprint" + << machineFingerprintString << "for node" << nodeUUIDString << ". isAdmin:" << isAdmin; - emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString); + emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString, isAdmin); } void NodeList::setRequestsDomainListData(bool isRequesting) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index c4564c0889..0e0a2fd6c8 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -120,7 +120,7 @@ signals: void receivedDomainServerList(); void ignoredNode(const QUuid& nodeID, bool enabled); void ignoreRadiusEnabledChanged(bool isIgnored); - void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint, bool isAdmin); private slots: void stopKeepalivePingTimer(); 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/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 758868ac63..76b98c6217 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -135,9 +135,10 @@ signals: /**jsdoc * Notifies scripts of the username and machine fingerprint associated with a UUID. + * Username and machineFingerprint will be their default constructor output if the requesting user isn't an admin. * @function Users.usernameFromIDReply */ - void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint, bool isAdmin); /**jsdoc * Notifies scripts that a user has disconnected from the domain diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ec77894230..c29af35964 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: @@ -267,14 +279,12 @@ function populateUserList() { displayName: avatar.sessionDisplayName, userName: '', sessionId: id || '', - audioLevel: 0.0 + audioLevel: 0.0, + admin: false }; - // If the current user is an admin OR - // they're requesting their own username ("id" is blank)... - if (Users.canKick || !id) { - // Request the username from the given UUID - Users.requestUsernameFromID(id); - } + // Request the username, fingerprint, and admin status from the given UUID + // Username and fingerprint returns default constructor output if the requesting user isn't an admin + Users.requestUsernameFromID(id); // Request personal mute status and ignore status // from NodeList (as long as we're not requesting it for our own ID) if (id) { @@ -289,16 +299,19 @@ function populateUserList() { } // The function that handles the reply from the server -function usernameFromIDReply(id, username, machineFingerprint) { +function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { var data; // If the ID we've received is our ID... if (MyAvatar.sessionUUID === id) { // Set the data to contain specific strings. - data = ['', username]; - } else { + data = ['', username, isAdmin]; + } else if (Users.canKick) { // Set the data to contain the ID and the username (if we have one) // or fingerprint (if we don't have a username) string. - data = [id, username || machineFingerprint]; + data = [id, username || machineFingerprint, isAdmin]; + } else { + // Set the data to contain specific strings. + data = [id, '', isAdmin]; } print('Username Data:', JSON.stringify(data)); // Ship the data off to QML @@ -576,6 +589,63 @@ function createAudioInterval() { }, AUDIO_LEVEL_UPDATE_INTERVAL_MS); } +// +// Manage the connection between the button and the window. +// +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var buttonName = "pal"; +var button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/people.svg"), + visible: true, + hoverState: 2, + defaultState: 1, + 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); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + isWired = false; + } + triggerMapping.disable(); // It's ok if we disable twice. + 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); + } +} +function onClicked() { + if (!pal.visible) { + Users.requestsDomainListData = true; + populateUserList(); + pal.raise(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + createAudioInterval(); + palOpenedAt = new Date().getTime(); + } else { + off(); + } + pal.setVisible(!pal.visible); +} + function avatarDisconnected(nodeID) { // remove from the pal list pal.sendToQml({method: 'avatarDisconnected', params: [nodeID]});