diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 2fbe2f6dfe..2498af8010 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -12,6 +12,8 @@ #include "AssetServer.h" +#include + #include #include #include @@ -21,6 +23,7 @@ #include #include +#include #include #include "NetworkLogging.h" @@ -28,8 +31,44 @@ #include "SendAssetTask.h" #include "UploadAssetTask.h" +static const uint8_t MIN_CORES_FOR_MULTICORE = 4; +static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; +static const uint8_t CPU_AFFINITY_COUNT_LOW = 1; +static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000; + const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; +bool interfaceRunning() { + bool result = false; + +#ifdef Q_OS_WIN + QSharedMemory sharedMemory { getInterfaceSharedMemoryName() }; + result = sharedMemory.attach(QSharedMemory::ReadOnly); + if (result) { + sharedMemory.detach(); + } +#endif + return result; +} + +void updateConsumedCores() { + static bool wasInterfaceRunning = false; + bool isInterfaceRunning = interfaceRunning(); + // If state is unchanged, return early + if (isInterfaceRunning == wasInterfaceRunning) { + return; + } + + wasInterfaceRunning = isInterfaceRunning; + auto coreCount = std::thread::hardware_concurrency(); + if (isInterfaceRunning) { + coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW; + } + qDebug() << "Setting max consumed cores to " << coreCount; + setMaxCores(coreCount); +} + + AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), _taskPool(this) @@ -45,6 +84,20 @@ AssetServer::AssetServer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); + +#ifdef Q_OS_WIN + updateConsumedCores(); + QTimer* timer = new QTimer(this); + auto timerConnection = connect(timer, &QTimer::timeout, [] { + updateConsumedCores(); + }); + connect(qApp, &QCoreApplication::aboutToQuit, [this, timerConnection] { + disconnect(timerConnection); + }); + timer->setInterval(INTERFACE_RUNNING_CHECK_FREQUENCY_MS); + timer->setTimerType(Qt::CoarseTimer); + timer->start(); +#endif } void AssetServer::run() { diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 11cbd73970..cd866cecb2 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -262,8 +262,12 @@ void AvatarMixer::broadcastAvatarData() { // setup a PacketList for the avatarPackets auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); - if (avatar.getSessionDisplayName().isEmpty() && // We haven't set it yet... - nodeData->getReceivedIdentity()) { // ... but we have processed identity (with possible displayName). + if (nodeData->getAvatarSessionDisplayNameMustChange()) { + const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); + if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { + _sessionDisplayNames.remove(existingBaseDisplayName); + } + QString baseName = avatar.getDisplayName().trimmed(); const QRegularExpression curses{ "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. @@ -276,11 +280,14 @@ void AvatarMixer::broadcastAvatarData() { QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. int& highWater = soFar.first; nodeData->setBaseDisplayName(baseName); - avatar.setSessionDisplayName((highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName); + QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; + avatar.setSessionDisplayName(sessionDisplayName); highWater++; soFar.second++; // refcount nodeData->flagIdentityChange(); - sendIdentityPacket(nodeData, node); // Tell new node about its sessionUUID. Others will find out below. + nodeData->setAvatarSessionDisplayNameMustChange(false); + sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. Others will find out below. + qDebug() << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); } // this is an AGENT we have received head data from @@ -584,7 +591,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes if (avatar.processAvatarIdentity(identity)) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); - nodeData->setReceivedIdentity(); + nodeData->setAvatarSessionDisplayNameMustChange(true); } } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index f18cfdde1b..38db2e74d2 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -53,8 +53,8 @@ public: HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); } - bool getReceivedIdentity() const { return _gotIdentity; } - void setReceivedIdentity() { _gotIdentity = true; } + bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } + void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; } float getFullRateDistance() const { return _fullRateDistance; } @@ -112,7 +112,7 @@ private: std::unordered_set _hasReceivedFirstPacketsFrom; HRCTime _identityChangeTimestamp; - bool _gotIdentity { false }; + bool _avatarSessionDisplayNameMustChange{ false }; float _fullRateDistance = FLT_MAX; float _maxAvatarDistance = FLT_MAX; diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 09aefffdfe..c139a196d0 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index c6daf53b9a..2725ea1617 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -28,7 +28,7 @@ Item { property string uuid: "" property string displayName: "" property string userName: "" - property int displayTextHeight: 18 + property real displayNameTextPixelSize: 18 property int usernameTextHeight: 12 property real audioLevel: 0.0 property bool isMyCard: false @@ -55,18 +55,112 @@ Item { width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin height: childrenRect.height anchors.verticalCenter: parent.verticalCenter - // DisplayName Text + + // DisplayName field for my card + Rectangle { + id: myDisplayName + visible: isMyCard + // Size + width: parent.width + 70 + height: 35 + // Anchors + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: -10 + // Style + color: hifi.colors.textFieldLightBackground + border.color: hifi.colors.blueHighlight + border.width: 0 + TextInput { + id: myDisplayNameText + // Properties + text: thisNameCard.displayName + maximumLength: 256 + clip: true + // Size + width: parent.width + height: parent.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin + // Style + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueHighlight + selectedTextColor: "black" + // Text Positioning + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + // Signals + onEditingFinished: { + pal.sendToScript({method: 'displayNameUpdate', params: text}) + cursorPosition = 0 + focus = false + myDisplayName.border.width = 0 + color = hifi.colors.darkGray + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + myDisplayName.border.width = 1 + myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); + myDisplayNameText.focus = true + myDisplayNameText.color = "black" + } + onDoubleClicked: { + myDisplayNameText.selectAll(); + myDisplayNameText.focus = true; + } + onEntered: myDisplayName.color = hifi.colors.lightGrayText + onExited: myDisplayName.color = hifi.colors.textFieldLightBackground + } + // Edit pencil glyph + HiFiGlyphs { + id: editGlyph + text: hifi.glyphs.editPencil + // Text Size + size: displayNameTextPixelSize*1.5 + // Anchors + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: hifi.colors.baseGray + } + } + // Spacer for DisplayName for my card + Rectangle { + id: myDisplayNameSpacer + width: myDisplayName.width + // 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 + visible: !isMyCard // Size width: parent.width // Anchors anchors.top: parent.top // Text Size - size: thisNameCard.displayTextHeight + size: displayNameTextPixelSize // Text Positioning verticalAlignment: Text.AlignVCenter // Style @@ -83,7 +177,7 @@ Item { // Size width: parent.width // Anchors - anchors.top: displayNameText.bottom + anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameText.bottom // Text Size size: thisNameCard.usernameTextHeight // Text Positioning @@ -105,7 +199,7 @@ Item { Rectangle { id: nameCardVUMeter // Size - width: ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width + width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width height: 8 // Anchors anchors.top: spacer.bottom diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 3438baa217..549598dd2e 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -35,6 +35,17 @@ Rectangle { // Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server. // NOTE: if another script modifies the per-avatar gain, this value won't be accurate! property var gainSliderValueDB: ({}); + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage + z: 999 // Force the popup on top of everything else + } + function letterbox(message) { + letterboxMessage.text = message + letterboxMessage.visible = true + letterboxMessage.popupRadius = 0 + } // This is the container for the PAL Rectangle { @@ -176,8 +187,6 @@ Rectangle { TableViewColumn { visible: iAmAdmin role: "kick" - // The hacky spaces used to center text over the button, since I don't know how to apply a margin - // to column header text. title: "BAN" width: actionButtonWidth movable: false @@ -337,11 +346,6 @@ Rectangle { visible: iAmAdmin color: hifi.colors.lightGrayText } - function letterbox(message) { - letterboxMessage.text = message; - letterboxMessage.visible = true - - } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { color: hifi.colors.tableBackgroundLight @@ -402,9 +406,6 @@ Rectangle { onExited: adminHelpText.color = hifi.colors.redHighlight } } - LetterboxMessage { - id: letterboxMessage - } } function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml @@ -502,6 +503,10 @@ Rectangle { ignored = {}; gainSliderValueDB = {}; break; + case 'avatarDisconnected': + var sessionID = message.params[0]; + delete ignored[sessionID]; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index af8df853aa..e261e2198f 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -312,10 +312,11 @@ Item { readonly property string error: "=" readonly property string settings: "@" readonly property string trash: "{" - readonly property string objectGroup: "" + readonly property string objectGroup: "\ue000" readonly property string cm: "}" readonly property string msvg79: "~" readonly property string deg: "\\" readonly property string px: "|" + readonly property string editPencil: "\ue00d" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 503bb2fe58..45cea3f6c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1236,8 +1236,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int SEND_STATS_INTERVAL_MS = 10000; static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // setup the stats interval depending on if the 1s faster hearbeat was requested + static const QString FAST_STATS_ARG = "--fast-heartbeat"; + static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; static glm::vec3 lastAvatarPosition = myAvatar->getPosition(); static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1f5726acba..b0dc9922ff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -260,6 +260,10 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar } if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); + } else if (removalReason == KillAvatarReason::AvatarDisconnected) { + // remove from node sets, if present + DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); + DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); } _avatarFades.push_back(removedAvatar); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index c83f7a989c..d33dba535e 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -56,7 +56,7 @@ int main(int argc, const char* argv[]) { QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME"); + const QString& applicationName = getInterfaceSharedMemoryName(); bool instanceMightBeRunning = true; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index e35df161a9..058497a377 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1050,7 +1050,7 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) { } if (identity.displayName != _displayName) { - setDisplayName(identity.displayName); + _displayName = identity.displayName; hasIdentityChanged = true; } maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName); @@ -1097,6 +1097,9 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { void AvatarData::setDisplayName(const QString& displayName) { _displayName = displayName; + _sessionDisplayName = ""; + + sendIdentityPacket(); qCDebug(avatars) << "Changing display name for avatar to" << displayName; } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 98a563c4e5..0fcd207b94 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -847,6 +847,16 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { } } +void NodeList::removeFromIgnoreMuteSets(const QUuid& nodeID) { + // don't remove yourself, or nobody + if (!nodeID.isNull() && _sessionUUID != nodeID) { + QWriteLocker ignoredSetLocker{ &_ignoredSetLock }; + QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; + _ignoredNodeIDs.unsafe_erase(nodeID); + _personalMutedNodeIDs.unsafe_erase(nodeID); + } +} + bool NodeList::isIgnoringNode(const QUuid& nodeID) const { QReadLocker ignoredSetLocker{ &_ignoredSetLock }; return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 5c477303e2..c4564c0889 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -90,6 +90,8 @@ public: bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool isRequesting); + void removeFromIgnoreMuteSets(const QUuid& nodeID); + public slots: void reset(); void sendDomainServerCheckIn(); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index c012ed8f67..5cef6013d9 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -211,7 +211,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { HandControllerJoints, HasKillAvatarReason, SessionDisplayName, - Unignore + Unignore, + ImmediateSessionDisplayNameUpdates }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 0b6b7855b5..758868ac63 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -139,6 +139,13 @@ signals: */ void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + /**jsdoc + * Notifies scripts that a user has disconnected from the domain + * @function Users.avatar.avatarDisconnected + * @param {nodeID} NodeID The session ID of the avatar that has disconnected + */ + void avatarDisconnected(const QUuid& nodeID); + private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 89116aa924..a7e251f0e0 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include @@ -1022,4 +1024,54 @@ bool getProcessorInfo(ProcessorInfo& info) { #endif return false; -} \ No newline at end of file +} + + +const QString& getInterfaceSharedMemoryName() { + static const QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME"); + return applicationName; +} + +const std::vector& getAvailableCores() { + static std::vector availableCores; +#ifdef Q_OS_WIN + static std::once_flag once; + std::call_once(once, [&] { + DWORD_PTR defaultProcessAffinity = 0, defaultSystemAffinity = 0; + HANDLE process = GetCurrentProcess(); + GetProcessAffinityMask(process, &defaultProcessAffinity, &defaultSystemAffinity); + for (uint64_t i = 0; i < sizeof(DWORD_PTR) * BITS_IN_BYTE; ++i) { + DWORD_PTR coreMask = 1; + coreMask <<= i; + if (0 != (defaultSystemAffinity & coreMask)) { + availableCores.push_back(i); + } + } + }); +#endif + return availableCores; +} + +void setMaxCores(uint8_t maxCores) { +#ifdef Q_OS_WIN + HANDLE process = GetCurrentProcess(); + auto availableCores = getAvailableCores(); + if (availableCores.size() <= maxCores) { + DWORD_PTR currentProcessAffinity = 0, currentSystemAffinity = 0; + GetProcessAffinityMask(process, ¤tProcessAffinity, ¤tSystemAffinity); + SetProcessAffinityMask(GetCurrentProcess(), currentSystemAffinity); + return; + } + + DWORD_PTR newProcessAffinity = 0; + while (maxCores) { + int index = randIntInRange(0, (int)availableCores.size() - 1); + DWORD_PTR coreMask = 1; + coreMask <<= availableCores[index]; + newProcessAffinity |= coreMask; + availableCores.erase(availableCores.begin() + index); + maxCores--; + } + SetProcessAffinityMask(process, newProcessAffinity); +#endif +} diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 77204c9608..0f30842f47 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -231,4 +231,8 @@ struct ProcessorInfo { bool getProcessorInfo(ProcessorInfo& info); +const QString& getInterfaceSharedMemoryName(); + +void setMaxCores(uint8_t maxCores); + #endif // hifi_SharedUtil_h diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 6eda5cf036..aefbbf5f90 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -237,6 +237,11 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like data = message.params; Users.setAvatarGain(data['sessionId'], data['gain']); break; + case 'displayNameUpdate': + if (MyAvatar.displayName != message.params) { + MyAvatar.displayName = message.params; + } + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -646,7 +651,10 @@ function onClicked() { >>>>>>> b02a5f85ec6733b8c6cf5e3f19ce34ed8a375300 pal.setVisible(!pal.visible); } - +function avatarDisconnected(nodeID) { + // remove from the pal list + pal.sendToQml({method: 'avatarDisconnected', params: [nodeID]}); +} // // Button state. // @@ -659,6 +667,8 @@ button.clicked.connect(onClicked); pal.visibleChanged.connect(onVisibleChanged); pal.closed.connect(off); Users.usernameFromIDReply.connect(usernameFromIDReply); +Users.avatarDisconnected.connect(avatarDisconnected); + function clearLocalQMLDataAndClosePAL() { pal.sendToQml({ method: 'clearLocalQMLData' }); if (pal.visible) { @@ -681,6 +691,7 @@ Script.scriptEnding.connect(function () { Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.unsubscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); + Users.avatarDisconnected.disconnect(avatarDisconnected); off(); });