diff --git a/interface/resources/icons/tablet-icons/goto-a-msg.svg b/interface/resources/icons/tablet-icons/goto-a-msg.svg new file mode 100644 index 0000000000..f1f611adb9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-a-msg.svg @@ -0,0 +1,57 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/goto-msg.svg b/interface/resources/icons/tablet-icons/goto-i-msg.svg similarity index 100% rename from interface/resources/icons/tablet-icons/goto-msg.svg rename to interface/resources/icons/tablet-icons/goto-i-msg.svg diff --git a/interface/resources/icons/tablet-icons/wallet-a-msg.svg b/interface/resources/icons/tablet-icons/wallet-a-msg.svg new file mode 100644 index 0000000000..d51c3e99a2 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-a-msg.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/wallet-i-msg.svg b/interface/resources/icons/tablet-icons/wallet-i-msg.svg new file mode 100644 index 0000000000..676f97a966 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-i-msg.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index 8f391f24c0..c3d87ca2f5 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -131,7 +131,7 @@ Rectangle { print("Marketplace item tester unsupported assetType " + assetType); } }, - "trash": function(){ + "trash": function(resource, assetType){ if ("application" === assetType) { Commerce.uninstallApp(resource); } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 3b8e2c0f4d..2435678e77 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -93,7 +93,7 @@ Rectangle { console.log("Failed to get Available Updates", result.data.message); } else { sendToScript({method: 'purchases_availableUpdatesReceived', numUpdates: result.data.updates.length }); - root.numUpdatesAvailable = result.data.updates.length; + root.numUpdatesAvailable = result.total_entries; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 50208793fe..627da1d43f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -45,14 +45,6 @@ Item { onHistoryResult : { transactionHistoryModel.handlePage(null, result); } - - onAvailableUpdatesResult: { - if (result.status !== 'success') { - console.log("Failed to get Available Updates", result.data.message); - } else { - sendToScript({method: 'wallet_availableUpdatesReceived', numUpdates: result.data.updates.length }); - } - } } Connections { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3a34c87ef1..f23410bff9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3430,7 +3430,12 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); QString addressLookupString; if (urlIndex != -1) { - addressLookupString = arguments().value(urlIndex + 1); + QUrl url(arguments().value(urlIndex + 1)); + if (url.scheme() == URL_SCHEME_HIFIAPP) { + Setting::Handle("startUpApp").set(url.path()); + } else { + addressLookupString = url.toString(); + } } static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; @@ -7678,6 +7683,9 @@ void Application::openUrl(const QUrl& url) const { if (!url.isEmpty()) { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); + } else if (url.scheme() == URL_SCHEME_HIFIAPP) { + QmlCommerce commerce; + commerce.openSystemApp(url.path()); } else { // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0fdd246d7b..5705e7347c 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -234,11 +234,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { const SortableAvatar& sortData = *it; const auto avatar = std::static_pointer_cast(sortData.getAvatar()); - // TODO: to help us scale to more avatars it would be nice to not have to poll orb state here - // if the geometry is loaded then turn off the orb + // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update if (avatar->getSkeletonModel()->isLoaded()) { // remove the orb if it is there avatar->removeOrb(); + if (avatar->needsPhysicsUpdate()) { + _avatarsToChangeInPhysics.insert(avatar); + } } else { avatar->updateOrbPosition(); } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index df7ec93b6a..f2e6b68a0f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -116,6 +116,7 @@ MyAvatar::MyAvatar(QThread* thread) : _bodySensorMatrix(), _goToPending(false), _goToSafe(true), + _goToFeetAjustment(false), _goToPosition(), _goToOrientation(), _prevShouldDrawHead(true), @@ -498,7 +499,7 @@ void MyAvatar::update(float deltaTime) { setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); - if (_drawAverageFacingEnabled) { + if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); @@ -526,6 +527,11 @@ void MyAvatar::update(float deltaTime) { _physicsSafetyPending = getCollisionsEnabled(); _characterController.recomputeFlying(); // In case we've gone to into the sky. } + if (_goToFeetAjustment && _skeletonModelLoaded) { + auto feetAjustment = getWorldPosition() - getWorldFeetPosition(); + goToLocation(getWorldPosition() + feetAjustment); + _goToFeetAjustment = false; + } if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { // When needed and ready, arrange to check and fix. _physicsSafetyPending = false; @@ -1728,6 +1734,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _headBoneSet.clear(); _cauterizationNeedsUpdate = true; + _skeletonModelLoaded = false; std::shared_ptr skeletonConnection = std::make_shared(); *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() { @@ -1745,6 +1752,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModel->setCauterizeBoneSet(_headBoneSet); _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); initAnimGraph(); + _skeletonModelLoaded = true; } QObject::disconnect(*skeletonConnection); }); @@ -2945,46 +2953,10 @@ void MyAvatar::goToLocation(const QVariant& propertiesVar) { } void MyAvatar::goToFeetLocation(const glm::vec3& newPosition, - bool hasOrientation, const glm::quat& newOrientation, - bool shouldFaceLocation) { - - qCDebug(interfaceapp).nospace() << "MyAvatar goToFeetLocation - moving to " << newPosition.x << ", " - << newPosition.y << ", " << newPosition.z; - - ShapeInfo shapeInfo; - computeShapeInfo(shapeInfo); - glm::vec3 halfExtents = shapeInfo.getHalfExtents(); - glm::vec3 localFeetPos = shapeInfo.getOffset() - glm::vec3(0.0f, halfExtents.y + halfExtents.x, 0.0f); - glm::mat4 localFeet = createMatFromQuatAndPos(Quaternions::IDENTITY, localFeetPos); - - glm::mat4 worldFeet = createMatFromQuatAndPos(Quaternions::IDENTITY, newPosition); - - glm::mat4 avatarMat = worldFeet * glm::inverse(localFeet); - - glm::vec3 adjustedPosition = extractTranslation(avatarMat); - - _goToPending = true; - _goToPosition = adjustedPosition; - _goToOrientation = getWorldOrientation(); - if (hasOrientation) { - qCDebug(interfaceapp).nospace() << "MyAvatar goToFeetLocation - new orientation is " - << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; - - // orient the user to face the target - glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); - - if (shouldFaceLocation) { - quatOrientation = newOrientation * glm::angleAxis(PI, Vectors::UP); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - _goToPosition = adjustedPosition - quatOrientation * IDENTITY_FORWARD * DISTANCE_TO_USER; - } - - _goToOrientation = quatOrientation; - } - - emit transformChanged(); + bool hasOrientation, const glm::quat& newOrientation, + bool shouldFaceLocation) { + _goToFeetAjustment = true; + goToLocation(newPosition, hasOrientation, newOrientation, shouldFaceLocation); } void MyAvatar::goToLocation(const glm::vec3& newPosition, diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1dc0b3cd40..d7379a18c4 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1732,6 +1732,7 @@ private: bool _goToPending { false }; bool _physicsSafetyPending { false }; bool _goToSafe { true }; + bool _goToFeetAjustment { false }; glm::vec3 _goToPosition; glm::quat _goToOrientation; @@ -1807,6 +1808,7 @@ private: bool _haveReceivedHeightLimitsFromDomain { false }; int _disableHandTouchCount { 0 }; + bool _skeletonModelLoaded { false }; Setting::Handle _dominantHandSetting; Setting::Handle _headPitchSetting; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 29fa98fd1d..625998eb95 100644 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -119,6 +119,11 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const { return (_workloadRegion < workload::Region::R3 && !isDead()); } +bool OtherAvatar::needsPhysicsUpdate() const { + constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION; + return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST)); +} + void OtherAvatar::rebuildCollisionShape() { if (_motionState) { _motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h index 94b98f2747..5b72815757 100644 --- a/interface/src/avatar/OtherAvatar.h +++ b/interface/src/avatar/OtherAvatar.h @@ -43,6 +43,7 @@ public: void setWorkloadRegion(uint8_t region); bool shouldBeInPhysicsSimulation() const; + bool needsPhysicsUpdate() const; friend AvatarManager; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 7c5df0f3e3..aa39fdc1b9 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -47,6 +47,54 @@ QmlCommerce::QmlCommerce() { _appsPath = PathUtils::getAppDataPath() + "Apps/"; } + + + +void QmlCommerce::openSystemApp(const QString& appName) { + static QMap systemApps { + {"GOTO", "hifi/tablet/TabletAddressDialog.qml"}, + {"PEOPLE", "hifi/Pal.qml"}, + {"WALLET", "hifi/commerce/wallet/Wallet.qml"}, + {"MARKET", "/marketplace.html"} + }; + + static QMap systemInject{ + {"MARKET", "/scripts/system/html/js/marketplacesInject.js"} + }; + + + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + + QMap::const_iterator appPathIter = systemApps.find(appName); + if (appPathIter != systemApps.end()) { + if (appPathIter->contains(".qml", Qt::CaseInsensitive)) { + tablet->loadQMLSource(*appPathIter); + } + else if (appPathIter->contains(".html", Qt::CaseInsensitive)) { + QMap::const_iterator injectIter = systemInject.find(appName); + if (appPathIter == systemInject.end()) { + tablet->gotoWebScreen(NetworkingConstants::METAVERSE_SERVER_URL().toString() + *appPathIter); + } + else { + QString inject = "file:///" + qApp->applicationDirPath() + *injectIter; + tablet->gotoWebScreen(NetworkingConstants::METAVERSE_SERVER_URL().toString() + *appPathIter, inject); + } + } + else { + qCDebug(commerce) << "Attempted to open unknown type of URL!"; + return; + } + } + else { + qCDebug(commerce) << "Attempted to open unknown APP!"; + return; + } + + DependencyManager::get()->openTablet(); +} + + void QmlCommerce::getWalletStatus() { auto wallet = DependencyManager::get(); wallet->getWalletStatus(); @@ -360,7 +408,7 @@ bool QmlCommerce::openApp(const QString& itemHref) { // Read from the file to know what .html or .qml document to open QFile appFile(_appsPath + "/" + appHref.fileName()); if (!appFile.open(QIODevice::ReadOnly)) { - qCDebug(commerce) << "Couldn't open local .app.json file."; + qCDebug(commerce) << "Couldn't open local .app.json file:" << appFile; return false; } QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 79d8e82e71..bee30e1b62 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -24,6 +24,7 @@ class QmlCommerce : public QObject { public: QmlCommerce(); + void openSystemApp(const QString& appPath); signals: void walletStatusResult(uint walletStatus); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3e3c9da148..d9396ae4d1 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -176,7 +176,7 @@ int main(int argc, const char* argv[]) { if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) { if (parser.isSet(urlOption)) { QUrl url = QUrl(parser.value(urlOption)); - if (url.isValid() && url.scheme() == URL_SCHEME_HIFI) { + if (url.isValid() && (url.scheme() == URL_SCHEME_HIFI || url.scheme() == URL_SCHEME_HIFIAPP)) { qDebug() << "Writing URL to local socket"; socket.write(url.toString().toUtf8()); if (!socket.waitForBytesWritten(5000)) { diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index 8993842d6e..65b28bc5f8 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -13,6 +13,7 @@ #define hifi_AudioHRTF_h #include +#include static const int HRTF_AZIMUTHS = 72; // 360 / 5-degree steps static const int HRTF_TAPS = 64; // minimum-phase FIR coefficients @@ -56,6 +57,27 @@ public: void setGainAdjustment(float gain) { _gainAdjust = HRTF_GAIN * gain; }; float getGainAdjustment() { return _gainAdjust; } + // clear internal state, but retain settings + void reset() { + // FIR history + memset(_firState, 0, sizeof(_firState)); + + // integer delay history + memset(_delayState, 0, sizeof(_delayState)); + + // biquad history + memset(_bqState, 0, sizeof(_bqState)); + + // parameter history + _azimuthState = 0.0f; + _distanceState = 0.0f; + _gainState = 0.0f; + + // _gainAdjust is retained + + _silentState = true; + } + private: AudioHRTF(const AudioHRTF&) = delete; AudioHRTF& operator=(const AudioHRTF&) = delete; @@ -88,7 +110,7 @@ private: // global and local gain adjustment float _gainAdjust = HRTF_GAIN; - bool _silentState = false; + bool _silentState = true; }; #endif // AudioHRTF_h diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 3d782f69a7..dbbf8af4b9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -187,6 +187,13 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) { _entitiesScriptEngine->callEntityScriptMethod(entityID, "hoverLeaveEntity", event); }); + + connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) { + EntityItemPointer entity = getTree()->findEntityByID(entityID); + if (entity) { + entity->setScriptHasFinishedPreload(true); + } + }); } void EntityTreeRenderer::clear() { @@ -512,7 +519,11 @@ bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVectorisScriptPreloadFinished())) { // now check to see if the point contains our entity, this can be expensive if // the entity has a collision hull if (entity->contains(_avatarPosition)) { @@ -972,6 +983,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool entity->scriptHasUnloaded(); } if (shouldLoad) { + entity->setScriptHasFinishedPreload(false); _entitiesScriptEngine->loadEntityScript(entityID, resolveScriptURL(scriptUrl), reload); entity->scriptHasPreloaded(); } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 71e3a0ff27..5003e36e86 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -97,10 +97,10 @@ void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce withWriteLock([&] { auto entity = getEntity(); _position = entity->getWorldPosition(); - _dimensions = entity->getScaledDimensions(); + _dimensions = entity->getUnscaledDimensions(); // get unscaled to avoid scaling twice _orientation = entity->getWorldOrientation(); updateModelTransformAndBound(); - _renderTransform = getModelTransform(); + _renderTransform = getModelTransform(); // contains parent scale, if this entity scales with its parent if (_shape == entity::Sphere) { _renderTransform.postScale(SPHERE_ENTITY_SCALE); } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 8e382fabd4..7a0e61b29a 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -3197,3 +3197,26 @@ void EntityItem::setCloneIDs(const QVector& cloneIDs) { _cloneIDs = cloneIDs; }); } + +bool EntityItem::shouldPreloadScript() const { + return !_script.isEmpty() && ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); +} + +void EntityItem::scriptHasPreloaded() { + _loadedScript = _script; + _loadedScriptTimestamp = _scriptTimestamp; +} + +void EntityItem::scriptHasUnloaded() { + _loadedScript = ""; + _loadedScriptTimestamp = 0; + _scriptPreloadFinished = false; +} + +void EntityItem::setScriptHasFinishedPreload(bool value) { + _scriptPreloadFinished = value; +} + +bool EntityItem::isScriptPreloadFinished() { + return _scriptPreloadFinished; +} diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 490f9b9e6b..405b114ab3 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -470,10 +470,11 @@ public: /// We only want to preload if: /// there is some script, and either the script value or the scriptTimestamp /// value have changed since our last preload - bool shouldPreloadScript() const { return !_script.isEmpty() && - ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); } - void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; } - void scriptHasUnloaded() { _loadedScript = ""; _loadedScriptTimestamp = 0; } + bool shouldPreloadScript() const; + void scriptHasPreloaded(); + void scriptHasUnloaded(); + void setScriptHasFinishedPreload(bool value); + bool isScriptPreloadFinished(); bool getClientOnly() const { return _clientOnly; } virtual void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } @@ -584,6 +585,7 @@ protected: QString _script { ENTITY_ITEM_DEFAULT_SCRIPT }; /// the value of the script property QString _loadedScript; /// the value of _script when the last preload signal was sent quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload + bool _scriptPreloadFinished { false }; QString _serverScripts; /// keep track of time when _serverScripts property was last changed diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 31ff6da873..839e269fd4 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -32,6 +32,7 @@ namespace NetworkingConstants { const QString URL_SCHEME_ABOUT = "about"; const QString URL_SCHEME_HIFI = "hifi"; +const QString URL_SCHEME_HIFIAPP = "hifiapp"; const QString URL_SCHEME_QRC = "qrc"; const QString URL_SCHEME_FILE = "file"; const QString URL_SCHEME_HTTP = "http"; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ab6507b29c..be78a69b4c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1281,92 +1281,6 @@ QStringList Model::getJointNames() const { return isActive() ? getFBXGeometry().getJointNames() : QStringList(); } -class Blender : public QRunnable { -public: - - Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients); - - virtual void run() override; - -private: - - ModelPointer _model; - int _blendNumber; - Geometry::WeakPointer _geometry; - QVector _blendshapeCoefficients; -}; - -Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients) : - _model(model), - _blendNumber(blendNumber), - _geometry(geometry), - _blendshapeCoefficients(blendshapeCoefficients) { -} - -void Blender::run() { - QVector vertices; - QVector normalsAndTangents; - if (_model && _model->isLoaded()) { - DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); - int offset = 0; - int normalsAndTangentsOffset = 0; - auto meshes = _model->getFBXGeometry().meshes; - int meshIndex = 0; - foreach (const FBXMesh& mesh, meshes) { - auto modelMeshNormalsAndTangents = _model->_normalsAndTangents.find(meshIndex++); - if (mesh.blendshapes.isEmpty() || modelMeshNormalsAndTangents == _model->_normalsAndTangents.end()) { - continue; - } - - vertices += mesh.vertices; - normalsAndTangents += modelMeshNormalsAndTangents->second; - glm::vec3* meshVertices = vertices.data() + offset; - NormalType* meshNormalsAndTangents = normalsAndTangents.data() + normalsAndTangentsOffset; - offset += mesh.vertices.size(); - normalsAndTangentsOffset += modelMeshNormalsAndTangents->second.size(); - const float NORMAL_COEFFICIENT_SCALE = 0.01f; - for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) { - float vertexCoefficient = _blendshapeCoefficients.at(i); - const float EPSILON = 0.0001f; - if (vertexCoefficient < EPSILON) { - continue; - } - float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; - const FBXBlendshape& blendshape = mesh.blendshapes.at(i); - tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { - for (auto j = range.begin(); j < range.end(); j++) { - int index = blendshape.indices.at(j); - meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient; - - glm::vec3 normal = mesh.normals.at(index) + blendshape.normals.at(j) * normalCoefficient; - glm::vec3 tangent; - if (index < mesh.tangents.size()) { - tangent = mesh.tangents.at(index); - if ((int)j < blendshape.tangents.size()) { - tangent += blendshape.tangents.at(j) * normalCoefficient; - } - } -#if FBX_PACK_NORMALS - glm::uint32 finalNormal; - glm::uint32 finalTangent; - buffer_helpers::packNormalAndTangent(normal, tangent, finalNormal, finalTangent); -#else - const auto& finalNormal = normal; - const auto& finalTangent = tangent; -#endif - meshNormalsAndTangents[2 * index] = finalNormal; - meshNormalsAndTangents[2 * index + 1] = finalTangent; - } - }); - } - } - } - // post the result to the ModelBlender, which will dispatch to the model if still alive - QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", - Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), Q_ARG(QVector, vertices), - Q_ARG(QVector, normalsAndTangents)); -} - void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale) { if (forceRescale || _scaleToFit != scaleToFit || _scaleToFitDimensions != dimensions) { _scaleToFit = scaleToFit; @@ -1531,44 +1445,6 @@ void Model::updateClusterMatrices() { } } -bool Model::maybeStartBlender() { - if (isLoaded()) { - const FBXGeometry& fbxGeometry = getFBXGeometry(); - if (fbxGeometry.hasBlendedMeshes()) { - QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, _blendshapeCoefficients)); - return true; - } - } - return false; -} - -void Model::setBlendedVertices(int blendNumber, const QVector& vertices, const QVector& normalsAndTangents) { - if (!isLoaded() || blendNumber < _appliedBlendNumber || !_blendedVertexBuffersInitialized) { - return; - } - _appliedBlendNumber = blendNumber; - const FBXGeometry& fbxGeometry = getFBXGeometry(); - int index = 0; - int normalAndTangentIndex = 0; - for (int i = 0; i < fbxGeometry.meshes.size(); i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); - auto meshNormalsAndTangents = _normalsAndTangents.find(i); - const auto& buffer = _blendedVertexBuffers.find(i); - if (mesh.blendshapes.isEmpty() || meshNormalsAndTangents == _normalsAndTangents.end() || buffer == _blendedVertexBuffers.end()) { - continue; - } - - const auto vertexCount = mesh.vertices.size(); - const auto verticesSize = vertexCount * sizeof(glm::vec3); - buffer->second->resize(mesh.vertices.size() * sizeof(glm::vec3) + meshNormalsAndTangents->second.size() * sizeof(NormalType)); - buffer->second->setSubData(0, verticesSize, (gpu::Byte*) vertices.constData() + index * sizeof(glm::vec3)); - buffer->second->setSubData(verticesSize, meshNormalsAndTangents->second.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data() + normalAndTangentIndex * sizeof(NormalType)); - - index += vertexCount; - normalAndTangentIndex += meshNormalsAndTangents->second.size(); - } -} - void Model::deleteGeometry() { _deleteGeometryCounter++; _blendedVertexBuffers.clear(); @@ -1605,42 +1481,6 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { return _modelMeshRenderItemIDs; } -void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { - _blendedVertexBuffers[index] = std::make_shared(); - QVector normalsAndTangents; - normalsAndTangents.resize(2 * mesh.normals.size()); - - // Interleave normals and tangents - // Parallel version for performance - tbb::parallel_for(tbb::blocked_range(0, mesh.normals.size()), [&](const tbb::blocked_range& range) { - auto normalsRange = std::make_pair(mesh.normals.begin() + range.begin(), mesh.normals.begin() + range.end()); - auto tangentsRange = std::make_pair(mesh.tangents.begin() + range.begin(), mesh.tangents.begin() + range.end()); - auto normalsAndTangentsIt = normalsAndTangents.begin() + 2 * range.begin(); - - for (auto normalIt = normalsRange.first, tangentIt = tangentsRange.first; - normalIt != normalsRange.second; - ++normalIt, ++tangentIt) { -#if FBX_PACK_NORMALS - glm::uint32 finalNormal; - glm::uint32 finalTangent; - buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); -#else - const auto& finalNormal = *normalIt; - const auto& finalTangent = *tangentIt; -#endif - *normalsAndTangentsIt = finalNormal; - ++normalsAndTangentsIt; - *normalsAndTangentsIt = finalTangent; - ++normalsAndTangentsIt; - } - }); - const auto verticesSize = mesh.vertices.size() * sizeof(glm::vec3); - _blendedVertexBuffers[index]->resize(mesh.vertices.size() * sizeof(glm::vec3) + normalsAndTangents.size() * sizeof(NormalType)); - _blendedVertexBuffers[index]->setSubData(0, verticesSize, (const gpu::Byte*) mesh.vertices.constData()); - _blendedVertexBuffers[index]->setSubData(verticesSize, normalsAndTangents.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data()); - _normalsAndTangents[index] = normalsAndTangents; -} - void Model::createRenderItemSet() { assert(isLoaded()); const auto& meshes = _renderGeometry->getMeshes(); @@ -1774,6 +1614,164 @@ public: } }; + +class Blender : public QRunnable { +public: + + Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients); + + virtual void run() override; + +private: + + ModelPointer _model; + int _blendNumber; + Geometry::WeakPointer _geometry; + QVector _blendshapeCoefficients; +}; + +Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients) : + _model(model), + _blendNumber(blendNumber), + _geometry(geometry), + _blendshapeCoefficients(blendshapeCoefficients) { +} + +void Blender::run() { + QVector vertices; + QVector normalsAndTangents; + if (_model && _model->isLoaded()) { + DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); + int offset = 0; + int normalsAndTangentsOffset = 0; + auto meshes = _model->getFBXGeometry().meshes; + int meshIndex = 0; + foreach(const FBXMesh& mesh, meshes) { + auto modelMeshNormalsAndTangents = _model->_normalsAndTangents.find(meshIndex++); + if (mesh.blendshapes.isEmpty() || modelMeshNormalsAndTangents == _model->_normalsAndTangents.end()) { + continue; + } + + vertices += mesh.vertices; + normalsAndTangents += modelMeshNormalsAndTangents->second; + glm::vec3* meshVertices = vertices.data() + offset; + NormalType* meshNormalsAndTangents = normalsAndTangents.data() + normalsAndTangentsOffset; + offset += mesh.vertices.size(); + normalsAndTangentsOffset += modelMeshNormalsAndTangents->second.size(); + const float NORMAL_COEFFICIENT_SCALE = 0.01f; + for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) { + float vertexCoefficient = _blendshapeCoefficients.at(i); + const float EPSILON = 0.0001f; + if (vertexCoefficient < EPSILON) { + continue; + } + float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; + const FBXBlendshape& blendshape = mesh.blendshapes.at(i); + tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { + for (auto j = range.begin(); j < range.end(); j++) { + int index = blendshape.indices.at(j); + meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient; + + glm::vec3 normal = mesh.normals.at(index) + blendshape.normals.at(j) * normalCoefficient; + glm::vec3 tangent; + if (index < mesh.tangents.size()) { + tangent = mesh.tangents.at(index); + if ((int)j < blendshape.tangents.size()) { + tangent += blendshape.tangents.at(j) * normalCoefficient; + } + } +#if FBX_PACK_NORMALS + glm::uint32 finalNormal; + glm::uint32 finalTangent; + buffer_helpers::packNormalAndTangent(normal, tangent, finalNormal, finalTangent); +#else + const auto& finalNormal = normal; + const auto& finalTangent = tangent; +#endif + meshNormalsAndTangents[2 * index] = finalNormal; + meshNormalsAndTangents[2 * index + 1] = finalTangent; + } + }); + } + } + } + // post the result to the ModelBlender, which will dispatch to the model if still alive + QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", + Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), Q_ARG(QVector, vertices), + Q_ARG(QVector, normalsAndTangents)); +} + +bool Model::maybeStartBlender() { + if (isLoaded()) { + QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, _blendshapeCoefficients)); + return true; + } + return false; +} + +void Model::setBlendedVertices(int blendNumber, const QVector& vertices, const QVector& normalsAndTangents) { + if (!isLoaded() || blendNumber < _appliedBlendNumber || !_blendedVertexBuffersInitialized) { + return; + } + _appliedBlendNumber = blendNumber; + const FBXGeometry& fbxGeometry = getFBXGeometry(); + int index = 0; + int normalAndTangentIndex = 0; + for (int i = 0; i < fbxGeometry.meshes.size(); i++) { + const FBXMesh& mesh = fbxGeometry.meshes.at(i); + auto meshNormalsAndTangents = _normalsAndTangents.find(i); + const auto& buffer = _blendedVertexBuffers.find(i); + if (mesh.blendshapes.isEmpty() || meshNormalsAndTangents == _normalsAndTangents.end() || buffer == _blendedVertexBuffers.end()) { + continue; + } + + const auto vertexCount = mesh.vertices.size(); + const auto verticesSize = vertexCount * sizeof(glm::vec3); + buffer->second->resize(mesh.vertices.size() * sizeof(glm::vec3) + meshNormalsAndTangents->second.size() * sizeof(NormalType)); + buffer->second->setSubData(0, verticesSize, (gpu::Byte*) vertices.constData() + index * sizeof(glm::vec3)); + buffer->second->setSubData(verticesSize, meshNormalsAndTangents->second.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data() + normalAndTangentIndex * sizeof(NormalType)); + + index += vertexCount; + normalAndTangentIndex += meshNormalsAndTangents->second.size(); + } +} + +void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { + _blendedVertexBuffers[index] = std::make_shared(); + QVector normalsAndTangents; + normalsAndTangents.resize(2 * mesh.normals.size()); + + // Interleave normals and tangents + // Parallel version for performance + tbb::parallel_for(tbb::blocked_range(0, mesh.normals.size()), [&](const tbb::blocked_range& range) { + auto normalsRange = std::make_pair(mesh.normals.begin() + range.begin(), mesh.normals.begin() + range.end()); + auto tangentsRange = std::make_pair(mesh.tangents.begin() + range.begin(), mesh.tangents.begin() + range.end()); + auto normalsAndTangentsIt = normalsAndTangents.begin() + 2 * range.begin(); + + for (auto normalIt = normalsRange.first, tangentIt = tangentsRange.first; + normalIt != normalsRange.second; + ++normalIt, ++tangentIt) { +#if FBX_PACK_NORMALS + glm::uint32 finalNormal; + glm::uint32 finalTangent; + buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); +#else + const auto& finalNormal = *normalIt; + const auto& finalTangent = *tangentIt; +#endif + *normalsAndTangentsIt = finalNormal; + ++normalsAndTangentsIt; + *normalsAndTangentsIt = finalTangent; + ++normalsAndTangentsIt; + } + }); + const auto verticesSize = mesh.vertices.size() * sizeof(glm::vec3); + _blendedVertexBuffers[index]->resize(mesh.vertices.size() * sizeof(glm::vec3) + normalsAndTangents.size() * sizeof(NormalType)); + _blendedVertexBuffers[index]->setSubData(0, verticesSize, (const gpu::Byte*) mesh.vertices.constData()); + _blendedVertexBuffers[index]->setSubData(verticesSize, normalsAndTangents.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data()); + _normalsAndTangents[index] = normalsAndTangents; +} + ModelBlender::ModelBlender() : _pendingBlenders(0) { } @@ -1783,14 +1781,23 @@ ModelBlender::~ModelBlender() { void ModelBlender::noteRequiresBlend(ModelPointer model) { Lock lock(_mutex); - if (_pendingBlenders < QThread::idealThreadCount()) { - if (model->maybeStartBlender()) { - _pendingBlenders++; - return; - } + if (_modelsRequiringBlendsSet.find(model) == _modelsRequiringBlendsSet.end()) { + _modelsRequiringBlendsQueue.push(model); + _modelsRequiringBlendsSet.insert(model); } - _modelsRequiringBlends.insert(model); + if (_pendingBlenders < QThread::idealThreadCount()) { + while (!_modelsRequiringBlendsQueue.empty()) { + auto weakPtr = _modelsRequiringBlendsQueue.front(); + _modelsRequiringBlendsQueue.pop(); + _modelsRequiringBlendsSet.erase(weakPtr); + ModelPointer nextModel = weakPtr.lock(); + if (nextModel && nextModel->maybeStartBlender()) { + _pendingBlenders++; + return; + } + } + } } void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, QVector vertices, QVector normalsAndTangents) { @@ -1800,20 +1807,15 @@ void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, QVect { Lock lock(_mutex); _pendingBlenders--; - _modelsRequiringBlends.erase(model); - std::set> modelsToErase; - for (auto i = _modelsRequiringBlends.begin(); i != _modelsRequiringBlends.end(); i++) { - auto weakPtr = *i; + while (!_modelsRequiringBlendsQueue.empty()) { + auto weakPtr = _modelsRequiringBlendsQueue.front(); + _modelsRequiringBlendsQueue.pop(); + _modelsRequiringBlendsSet.erase(weakPtr); ModelPointer nextModel = weakPtr.lock(); if (nextModel && nextModel->maybeStartBlender()) { _pendingBlenders++; break; - } else { - modelsToErase.insert(weakPtr); } } - for (auto& weakPtr : modelsToErase) { - _modelsRequiringBlends.erase(weakPtr); - } } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 447f75dd9d..c763197bc6 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -530,7 +530,8 @@ private: ModelBlender(); virtual ~ModelBlender(); - std::set> _modelsRequiringBlends; + std::queue _modelsRequiringBlendsQueue; + std::set> _modelsRequiringBlendsSet; int _pendingBlenders; Mutex _mutex; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index cfd155e14b..4d395070d6 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -2442,6 +2442,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co // if we got this far, then call the preload method callEntityScriptMethod(entityID, "preload"); + emit entityScriptPreloadFinished(entityID); + _occupiedScriptURLs.remove(entityScript); processDeferredEntityLoads(entityScript, entityID); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 08e2c492e8..17afd3dbbd 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -712,6 +712,13 @@ signals: // script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) void entityScriptDetailsUpdated(); + /**jsdoc + * @function Script.entityScriptPreloadFinished + * @returns {Signal} + */ + // Emitted when an entity script has finished running preload + void entityScriptPreloadFinished(const EntityItemID& entityID); + protected: void init(); diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index dab377911b..83d99cd42b 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -107,7 +107,9 @@ function AppUi(properties) { that.notificationPollCaresAboutSince = false; that.notificationInitialCallbackMade = false; that.notificationDisplayBanner = function (message) { - Window.displayAnnouncement(message); + if (!that.isOpen) { + Window.displayAnnouncement(message); + } }; // // END Notification Handling Defaults @@ -118,6 +120,7 @@ function AppUi(properties) { // Set isOpen, wireEventBridge, set buttonActive as appropriate, // and finally call onOpened() or onClosed() IFF defined. that.setCurrentVisibleScreenMetadata(type, url); + if (that.checkIsOpen(type, url)) { that.wireEventBridge(true); if (!that.isOpen) { @@ -155,17 +158,21 @@ function AppUi(properties) { return; } - // User is "appearing offline" - if (GlobalServices.findableBy === "none") { + // User is "appearing offline" or is offline + if (GlobalServices.findableBy === "none" || Account.username === "") { that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); return; } var url = METAVERSE_BASE + that.notificationPollEndpoint; + var settingsKey = "notifications/" + that.buttonName + "/lastPoll"; + var currentTimestamp = new Date().getTime(); + var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); if (that.notificationPollCaresAboutSince) { - url = url + "&since=" + (new Date().getTime()); + url = url + "&since=" + lastPollTimestamp/1000; } + Settings.setValue(settingsKey, currentTimestamp); console.debug(that.buttonName, 'polling for notifications at endpoint', url); @@ -193,17 +200,18 @@ function AppUi(properties) { } else { concatenatedServerResponse = concatenatedServerResponse.concat(that.notificationDataProcessPage(response)); currentDataPageToRetrieve++; - request({ uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); + request({ json: true, uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); } } - request({ uri: url }, requestCallback); + request({ json: true, uri: url }, requestCallback); }; // This won't do anything if there isn't a notification endpoint set that.notificationPoll(); - function availabilityChanged() { + function restartNotificationPoll() { + that.notificationInitialCallbackMade = false; if (that.notificationPollTimeout) { Script.clearTimeout(that.notificationPollTimeout); that.notificationPollTimeout = false; @@ -303,7 +311,8 @@ function AppUi(properties) { } : that.ignore; that.onScriptEnding = function onScriptEnding() { // Close if necessary, clean up any remaining handlers, and remove the button. - GlobalServices.findableByChanged.disconnect(availabilityChanged); + GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); + GlobalServices.findableByChanged.disconnect(restartNotificationPoll); if (that.isOpen) { that.close(); } @@ -323,6 +332,13 @@ function AppUi(properties) { that.tablet.screenChanged.connect(that.onScreenChanged); that.button.clicked.connect(that.onClicked); Script.scriptEnding.connect(that.onScriptEnding); - GlobalServices.findableByChanged.connect(availabilityChanged); + GlobalServices.findableByChanged.connect(restartNotificationPoll); + GlobalServices.myUsernameChanged.connect(restartNotificationPoll); + if (that.buttonName == Settings.getValue("startUpApp")) { + Settings.setValue("startUpApp", ""); + Script.setTimeout(function () { + that.open(); + }, 1000); + } } module.exports = AppUi; diff --git a/scripts/modules/request.js b/scripts/modules/request.js index 3516554567..d0037f9b43 100644 --- a/scripts/modules/request.js +++ b/scripts/modules/request.js @@ -19,7 +19,7 @@ module.exports = { // ------------------------------------------------------------------ request: function (options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. - var httpRequest = new XMLHttpRequest(), key; + var httpRequest = new XMLHttpRequest(), key; // QT bug: apparently doesn't handle onload. Workaround using readyState. httpRequest.onreadystatechange = function () { var READY_STATE_DONE = 4; @@ -72,7 +72,7 @@ module.exports = { } httpRequest.open(options.method, options.uri, true); httpRequest.send(options.body || null); - } + } }; // =========================================================================================== diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index b12191b00c..5b91afea33 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -474,9 +474,6 @@ function fromQml(message) { Window.location = "hifi://BankOfHighFidelity"; } break; - case 'wallet_availableUpdatesReceived': - // NOP - break; case 'http.request': // Handled elsewhere, don't log. break; @@ -491,32 +488,65 @@ function walletOpened() { Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerMapping.enable(); triggerPressMapping.enable(); + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); } function walletClosed() { off(); } -// -// Manage the connection between the button and the window. -// +function notificationDataProcessPage(data) { + return data.data.history; +} + +var shouldShowDot = false; +function notificationPollCallback(historyArray) { + if (!ui.isOpen) { + var notificationCount = historyArray.length; + shouldShowDot = shouldShowDot || notificationCount > 0; + ui.messagesWaiting(shouldShowDot); + + if (notificationCount > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = "You have " + notificationCount + " unread wallet " + + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open WALLET to see all activity."; + ui.notificationDisplayBanner(message); + } else { + for (var i = 0; i < notificationCount; i++) { + message = '"' + (historyArray[i].message) + '" ' + + "Open WALLET to see all activity."; + ui.notificationDisplayBanner(message); + } + } + } + } +} + +function isReturnedDataEmpty(data) { + var historyArray = data.data.history; + return historyArray.length === 0; +} + var DEVELOPER_MENU = "Developer"; var MARKETPLACE_ITEM_TESTER_LABEL = "Marketplace Item Tester"; var MARKETPLACE_ITEM_TESTER_QML_SOURCE = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; function installMarketplaceItemTester() { if (!Menu.menuExists(DEVELOPER_MENU)) { - Menu.addMenu(DEVELOPER_MENU); + Menu.addMenu(DEVELOPER_MENU); } if (!Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL)) { - Menu.addMenuItem({ menuName: DEVELOPER_MENU, - menuItemName: MARKETPLACE_ITEM_TESTER_LABEL, - isCheckable: false }) + Menu.addMenuItem({ + menuName: DEVELOPER_MENU, + menuItemName: MARKETPLACE_ITEM_TESTER_LABEL, + isCheckable: false + }); } Menu.menuItemEvent.connect(function (menuItem) { if (menuItem === MARKETPLACE_ITEM_TESTER_LABEL) { - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.loadQMLSource(MARKETPLACE_ITEM_TESTER_QML_SOURCE); + ui.open(MARKETPLACE_ITEM_TESTER_QML_SOURCE); } }); } @@ -539,7 +569,13 @@ function startup() { home: WALLET_QML_SOURCE, onOpened: walletOpened, onClosed: walletClosed, - onMessage: fromQml + onMessage: fromQml, + notificationPollEndpoint: "/api/v1/commerce/history?per_page=10", + notificationPollTimeoutMs: 300000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: true }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); installMarketplaceItemTester(); diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index d590545532..15da1537a1 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -73,21 +73,22 @@ Script.include("/~/system/libraries/utils.js"); method: "clearSelection", hand: hand })); + } else { + if (this.selectedTarget.type === Picks.INTERSECTED_ENTITY) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectEntity", + entityID: this.selectedTarget.objectID, + hand: hand + })); + } else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectOverlay", + overlayID: this.selectedTarget.objectID, + hand: hand + })); + } } } - if (this.selectedTarget.type === Picks.INTERSECTED_ENTITY) { - Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ - method: "selectEntity", - entityID: this.selectedTarget.objectID, - hand: hand - })); - } else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) { - Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ - method: "selectOverlay", - overlayID: this.selectedTarget.objectID, - hand: hand - })); - } this.triggerClicked = true; } diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 5dee36d147..d205d368dd 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -32,7 +32,7 @@ var WAITING_INTERVAL = 100; // ms var CONNECTING_INTERVAL = 100; // ms var MAKING_CONNECTION_TIMEOUT = 800; // ms - var CONNECTING_TIME = 1600; // ms + var CONNECTING_TIME = 100; // ms One interval. var PARTICLE_RADIUS = 0.15; // m var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz var HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index cc5ff99673..85cd499d20 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -138,7 +138,6 @@ function onUsernameChanged() { } } -var userHasUpdates = false; function sendCommerceSettings() { ui.sendToHtml({ type: "marketplaces", @@ -148,7 +147,7 @@ function sendCommerceSettings() { userIsLoggedIn: Account.loggedIn, walletNeedsSetup: Wallet.walletStatus === 1, metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: userHasUpdates + messagesWaiting: shouldShowDot } }); } @@ -924,10 +923,9 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { removeOverlays(); } break; - case 'wallet_availableUpdatesReceived': case 'purchases_availableUpdatesReceived': - userHasUpdates = message.numUpdates > 0; - ui.messagesWaiting(userHasUpdates); + shouldShowDot = message.numUpdates > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); break; case 'purchases_updateWearables': var currentlyWornWearables = []; @@ -1085,9 +1083,41 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); }; -// -// Manage the connection between the button and the window. -// +function notificationDataProcessPage(data) { + return data.data.updates; +} + +var shouldShowDot = false; +function notificationPollCallback(updatesArray) { + shouldShowDot = shouldShowDot || updatesArray.length > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + + if (updatesArray.length > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = updatesArray.length + " of your purchased items " + + (updatesArray.length === 1 ? "has an update " : "have updates ") + + "available. Open MARKET to update."; + ui.notificationDisplayBanner(message); + + ui.notificationPollCaresAboutSince = true; + } else { + for (var i = 0; i < updatesArray.length; i++) { + message = "Update available for \"" + + updatesArray[i].base_item_title + "\"." + + "Open MARKET to update."; + ui.notificationDisplayBanner(message); + } + } + } +} + +function isReturnedDataEmpty(data) { + var historyArray = data.data.updates; + return historyArray.length === 0; +} + + var BUTTON_NAME = "MARKET"; var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. @@ -1099,7 +1129,13 @@ function startup() { inject: MARKETPLACES_INJECT_SCRIPT_URL, home: MARKETPLACE_URL_INITIAL, onScreenChanged: onTabletScreenChanged, - onMessage: onQmlMessageReceived + onMessage: onQmlMessageReceived, + notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", + notificationPollTimeoutMs: 300000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: false // Changes to true after first poll }); ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 85898c28fb..a2ebae1a33 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -823,46 +823,40 @@ function notificationDataProcessPage(data) { } var shouldShowDot = false; -var storedOnlineUsersArray = []; +var pingPong = false; +var storedOnlineUsers = {}; function notificationPollCallback(connectionsArray) { // // START logic for handling online/offline user changes // - var i, j; - var newlyOnlineConnectionsArray = []; - for (i = 0; i < connectionsArray.length; i++) { - var currentUser = connectionsArray[i]; + pingPong = !pingPong; + var newOnlineUsers = 0; + var message; - if (connectionsArray[i].online) { - var indexOfStoredOnlineUser = -1; - for (j = 0; j < storedOnlineUsersArray.length; j++) { - if (currentUser.username === storedOnlineUsersArray[j].username) { - indexOfStoredOnlineUser = j; - break; - } - } - // If the user record isn't already presesnt inside `storedOnlineUsersArray`... - if (indexOfStoredOnlineUser < 0) { - storedOnlineUsersArray.push(currentUser); - newlyOnlineConnectionsArray.push(currentUser); - } - } else { - var indexOfOfflineUser = -1; - for (j = 0; j < storedOnlineUsersArray.length; j++) { - if (currentUser.username === storedOnlineUsersArray[j].username) { - indexOfOfflineUser = j; - break; - } - } - if (indexOfOfflineUser >= 0) { - storedOnlineUsersArray.splice(indexOfOfflineUser); - } + connectionsArray.forEach(function (user) { + var stored = storedOnlineUsers[user.username]; + var storedOrNew = stored || user; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } + + newOnlineUsers++; + storedOnlineUsers[user.username] = user; + + if (!ui.isOpen && ui.notificationInitialCallbackMade) { + message = user.username + " is available in " + + user.location.root.name + ". Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); + } + }); + var key; + for (key in storedOnlineUsers) { + if (storedOnlineUsers[key].pingPong !== pingPong) { + delete storedOnlineUsers[key]; } } - // If there's new data, the light should turn on. - // If the light is already on and you have connections online, the light should stay on. - // In all other cases, the light should turn off or stay off. - shouldShowDot = newlyOnlineConnectionsArray.length > 0 || (storedOnlineUsersArray.length > 0 && shouldShowDot); + shouldShowDot = newOnlineUsers > 0 || (Object.keys(storedOnlineUsers).length > 0 && shouldShowDot); // // END logic for handling online/offline user changes // @@ -874,19 +868,10 @@ function notificationPollCallback(connectionsArray) { shouldShowDot: shouldShowDot }); - if (newlyOnlineConnectionsArray.length > 0) { - var message; - if (!ui.notificationInitialCallbackMade) { - message = newlyOnlineConnectionsArray.length + " of your connections " + - (newlyOnlineConnectionsArray.length === 1 ? "is" : "are") + " online. Open PEOPLE to join them!"; - ui.notificationDisplayBanner(message); - } else { - for (i = 0; i < newlyOnlineConnectionsArray.length; i++) { - message = newlyOnlineConnectionsArray[i].username + " is available in " + - newlyOnlineConnectionsArray[i].location.root.name + ". Open PEOPLE to join them!"; - ui.notificationDisplayBanner(message); - } - } + if (newOnlineUsers > 0 && !ui.notificationInitialCallbackMade) { + message = newOnlineUsers + " of your connections " + + (newOnlineUsers === 1 ? "is" : "are") + " available online. Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); } } } @@ -904,7 +889,7 @@ function startup() { onOpened: palOpened, onClosed: off, onMessage: fromQml, - notificationPollEndpoint: "/api/v1/users?filter=connections&per_page=10", + notificationPollEndpoint: "/api/v1/users?filter=connections&status=online&per_page=10", notificationPollTimeoutMs: 60000, notificationDataProcessPage: notificationDataProcessPage, notificationPollCallback: notificationPollCallback, diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 804f838d04..6d8ba3a927 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -15,118 +15,121 @@ // (function () { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; var AppUi = Script.require('appUi'); -var DEBUG = false; -function debug() { - if (!DEBUG) { - return; - } - print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); -} - -var stories = {}, pingPong = false; -function expire(id) { - var options = { - uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, - method: 'PUT', - json: true, - body: {expire: "true"} - }; - request(options, function (error, response) { - debug('expired story', options, 'error:', error, 'response:', response); - if (error || (response.status !== 'success')) { - print("ERROR expiring story: ", error || response.status); - } - }); -} -var PER_PAGE_DEBUG = 10; -var PER_PAGE_NORMAL = 100; -function pollForAnnouncements() { - // We could bail now if !Account.isLoggedIn(), but what if we someday have system-wide announcments? - var actions = 'announcement'; - var count = DEBUG ? PER_PAGE_DEBUG : PER_PAGE_NORMAL; - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(Window.protocolSignature()), - 'per_page=' + count - ]; - var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); - request({ - uri: url - }, function (error, data) { - debug(url, error, data); - if (error || (data.status !== 'success')) { - print("Error: unable to get", url, error || data.status); - return; - } - var didNotify = false, key; - pingPong = !pingPong; - data.user_stories.forEach(function (story) { - var stored = stories[story.id], storedOrNew = stored || story; - debug('story exists:', !!stored, storedOrNew); - if ((storedOrNew.username === Account.username) && (storedOrNew.place_name !== location.placename)) { - if (storedOrNew.audience === 'for_connections') { // Only expire if we haven't already done so. - expire(story.id); - } - return; // before marking - } - storedOrNew.pingPong = pingPong; - if (stored) { // already seen - return; - } - stories[story.id] = story; - var message = story.username + " " + story.action_string + " in " + - story.place_name + ". Open GOTO to join them."; - Window.displayAnnouncement(message); - didNotify = true; - }); - for (key in stories) { // Any story we were tracking that was not marked, has expired. - if (stories[key].pingPong !== pingPong) { - debug('removing story', key); - delete stories[key]; - } - } - if (didNotify) { - ui.messagesWaiting(true); - if (HMD.isHandControllerAvailable()) { - var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands - Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); - } - } else if (!Object.keys(stories).length) { // If there's nothing being tracked, then any messageWaiting has expired. - ui.messagesWaiting(false); - } - }); -} -var MS_PER_SEC = 1000; -var DEBUG_POLL_TIME_SEC = 10; -var NORMAL_POLL_TIME_SEC = 60; -var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? DEBUG_POLL_TIME_SEC : NORMAL_POLL_TIME_SEC) * MS_PER_SEC; -var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); function gotoOpened() { - ui.messagesWaiting(false); + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); +} + +function notificationDataProcessPage(data) { + return data.user_stories; +} + +var shouldShowDot = false; +var pingPong = false; +var storedAnnouncements = {}; +var storedFeaturedStories = {}; +var message; +function notificationPollCallback(userStoriesArray) { + // + // START logic for keeping track of new info + // + pingPong = !pingPong; + var totalNewStories = 0; + var shouldNotifyIndividually = !ui.isOpen && ui.notificationInitialCallbackMade; + userStoriesArray.forEach(function (story) { + if (story.audience !== "for_connections" && + story.audience !== "for_feed") { + return; + } + + var stored = storedAnnouncements[story.id] || storedFeaturedStories[story.id]; + var storedOrNew = stored || story; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } + + totalNewStories++; + + if (story.audience === "for_connections") { + storedAnnouncements[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " says something is happening in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } else if (story.audience === "for_feed") { + storedFeaturedStories[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " invites you to an event in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } + }); + var key; + for (key in storedAnnouncements) { + if (storedAnnouncements[key].pingPong !== pingPong) { + delete storedAnnouncements[key]; + } + } + for (key in storedFeaturedStories) { + if (storedFeaturedStories[key].pingPong !== pingPong) { + delete storedFeaturedStories[key]; + } + } + // + // END logic for keeping track of new info + // + + var totalStories = Object.keys(storedAnnouncements).length + + Object.keys(storedFeaturedStories).length; + shouldShowDot = totalNewStories > 0 || (totalStories > 0 && shouldShowDot); + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + + if (totalStories > 0 && !ui.isOpen && !ui.notificationInitialCallbackMade) { + message = "There " + (totalStories === 1 ? "is " : "are ") + totalStories + " event" + + (totalStories === 1 ? "" : "s") + " to know about. " + + "Open GOTO to see " + (totalStories === 1 ? "it" : "them") + "."; + ui.notificationDisplayBanner(message); + } +} + +function isReturnedDataEmpty(data) { + var storiesArray = data.user_stories; + return storiesArray.length === 0; } var ui; var GOTO_QML_SOURCE = "hifi/tablet/TabletAddressDialog.qml"; var BUTTON_NAME = "GOTO"; function startup() { + var options = [ + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()), + 'per_page=10' + ]; + var endpoint = '/api/v1/user_stories?' + options.join('&'); + ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 8, onOpened: gotoOpened, - home: GOTO_QML_SOURCE + home: GOTO_QML_SOURCE, + notificationPollEndpoint: endpoint, + notificationPollTimeoutMs: 60000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: false }); } -function shutdown() { - Script.clearInterval(pollTimer); -} - startup(); -Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/server-console/resources/tray-menu-notification.png b/server-console/resources/tray-menu-notification.png new file mode 100644 index 0000000000..0d6e15752f Binary files /dev/null and b/server-console/resources/tray-menu-notification.png differ diff --git a/server-console/src/main.js b/server-console/src/main.js index 92ebdbf36c..95b5935255 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -29,92 +29,44 @@ const updater = require('./modules/hf-updater.js'); const Config = require('./modules/config').Config; const hfprocess = require('./modules/hf-process.js'); + +global.log = require('electron-log'); + const Process = hfprocess.Process; const ACMonitorProcess = hfprocess.ACMonitorProcess; const ProcessStates = hfprocess.ProcessStates; const ProcessGroup = hfprocess.ProcessGroup; const ProcessGroupStates = hfprocess.ProcessGroupStates; +const hfApp = require('./modules/hf-app.js'); +const GetBuildInfo = hfApp.getBuildInfo; +const StartInterface = hfApp.startInterface; +const getRootHifiDataDirectory = hfApp.getRootHifiDataDirectory; +const getDomainServerClientResourcesDirectory = hfApp.getDomainServerClientResourcesDirectory; +const getAssignmentClientResourcesDirectory = hfApp.getAssignmentClientResourcesDirectory; +const getApplicationDataDirectory = hfApp.getApplicationDataDirectory; + + const osType = os.type(); const appIcon = path.join(__dirname, '../resources/console.png'); +const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notification.png'); + const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; -function getBuildInfo() { - var buildInfoPath = null; +const buildInfo = GetBuildInfo(); - if (osType == 'Windows_NT') { - buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); - } else if (osType == 'Darwin') { - var contentPath = ".app/Contents/"; - var contentEndIndex = __dirname.indexOf(contentPath); - if (contentEndIndex != -1) { - // this is an app bundle - var appPath = __dirname.substring(0, contentEndIndex) + ".app"; - buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); - } - } - - const DEFAULT_BUILD_INFO = { - releaseType: "", - buildIdentifier: "dev", - buildNumber: "0", - stableBuild: "0", - organization: "High Fidelity - dev", - appUserModelId: "com.highfidelity.sandbox-dev" - }; - var buildInfo = DEFAULT_BUILD_INFO; - - if (buildInfoPath) { - try { - buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); - } catch (e) { - buildInfo = DEFAULT_BUILD_INFO; - } - } - - return buildInfo; -} -const buildInfo = getBuildInfo(); - -function getRootHifiDataDirectory(local) { - var organization = buildInfo.organization; - if (osType == 'Windows_NT') { - if (local) { - return path.resolve(osHomeDir(), 'AppData/Local', organization); - } else { - return path.resolve(osHomeDir(), 'AppData/Roaming', organization); - } - } else if (osType == 'Darwin') { - return path.resolve(osHomeDir(), 'Library/Application Support', organization); - } else { - return path.resolve(osHomeDir(), '.local/share/', organization); - } -} - -function getDomainServerClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/domain-server'); -} - -function getAssignmentClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/assignment-client'); -} - -function getApplicationDataDirectory(local) { - return path.join(getRootHifiDataDirectory(local), '/Server Console'); -} // Update lock filepath const UPDATER_LOCK_FILENAME = ".updating"; const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME; // Configure log -global.log = require('electron-log'); const oldLogFile = path.join(getApplicationDataDirectory(), '/log.txt'); const logFile = path.join(getApplicationDataDirectory(true), '/log.txt'); if (oldLogFile != logFile && fs.existsSync(oldLogFile)) { @@ -149,15 +101,23 @@ const configPath = path.join(getApplicationDataDirectory(), 'config.json'); var userConfig = new Config(); userConfig.load(configPath); - const ipcMain = electron.ipcMain; + +function isServerInstalled() { + return interfacePath && userConfig.get("serverInstalled", true); +} + +function isInterfaceInstalled() { + return dsPath && acPath && userConfig.get("interfaceInstalled", true); +} + var isShuttingDown = false; function shutdown() { log.debug("Normal shutdown (isShuttingDown: " + isShuttingDown + ")"); if (!isShuttingDown) { // if the home server is running, show a prompt before quit to ask if the user is sure - if (homeServer.state == ProcessGroupStates.STARTED) { + if (isServerInstalled() && homeServer.state == ProcessGroupStates.STARTED) { log.debug("Showing shutdown dialog."); dialog.showMessageBox({ type: 'question', @@ -184,6 +144,9 @@ function shutdownCallback(idx) { if (idx == 0 && !isShuttingDown) { isShuttingDown = true; + log.debug("Stop tray polling."); + trayNotifications.stopPolling(); + log.debug("Saving user config"); userConfig.save(configPath); @@ -191,31 +154,37 @@ function shutdownCallback(idx) { log.debug("Closing log window"); logWindow.close(); } - if (homeServer) { - log.debug("Stoping home server"); - homeServer.stop(); - } - updateTrayMenu(homeServer.state); + if (isServerInstalled()) { + if (homeServer) { + log.debug("Stoping home server"); + homeServer.stop(); - if (homeServer.state == ProcessGroupStates.STOPPED) { - // if the home server is already down, take down the server console now - log.debug("Quitting."); - app.exit(0); - } else { - // if the home server is still running, wait until we get a state change or timeout - // before quitting the app - log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(function() { - app.exit(0); - }, 5000); - homeServer.on('state-update', function(processGroup) { - if (processGroup.state == ProcessGroupStates.STOPPED) { - clearTimeout(timeoutID); + updateTrayMenu(homeServer.state); + + if (homeServer.state == ProcessGroupStates.STOPPED) { + // if the home server is already down, take down the server console now log.debug("Quitting."); app.exit(0); + } else { + // if the home server is still running, wait until we get a state change or timeout + // before quitting the app + log.debug("Server still shutting down. Waiting"); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); + homeServer.on('state-update', function(processGroup) { + if (processGroup.state == ProcessGroupStates.STOPPED) { + clearTimeout(timeoutID); + log.debug("Quitting."); + app.exit(0); + } + }); } - }); + } + } + else { + app.exit(0); } } } @@ -351,20 +320,6 @@ function openLogDirectory() { app.on('window-all-closed', function() { }); -function startInterface(url) { - var argArray = []; - - // check if we have a url parameter to include - if (url) { - argArray = ["--url", url]; - } - - // create a new Interface instance - Interface makes sure only one is running at a time - var pInterface = new Process('interface', interfacePath, argArray); - pInterface.detached = true; - pInterface.start(); -} - var tray = null; global.homeServer = null; global.domainServer = null; @@ -372,6 +327,18 @@ global.acMonitor = null; global.userConfig = userConfig; global.openLogDirectory = openLogDirectory; +const hfNotifications = require('./modules/hf-notifications.js'); +const HifiNotifications = hfNotifications.HifiNotifications; +const HifiNotificationType = hfNotifications.NotificationType; + +var pendingNotifications = {} +function notificationCallback(notificationType, pending = true) { + pendingNotifications[notificationType] = pending; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); +} + +var trayNotifications = new HifiNotifications(userConfig, notificationCallback); + var LogWindow = function(ac, ds) { this.ac = ac; this.ds = ds; @@ -407,7 +374,7 @@ LogWindow.prototype = { function visitSandboxClicked() { if (interfacePath) { - startInterface('hifi://localhost'); + StartInterface('hifi://localhost'); } else { // show an error to say that we can't go home without an interface instance dialog.showErrorBox("Client Not Found", binaryMissingMessage("High Fidelity client", "Interface", false)); @@ -425,6 +392,48 @@ var labels = { label: 'Version - ' + buildInfo.buildIdentifier, enabled: false }, + showNotifications: { + label: 'Show Notifications', + type: 'checkbox', + checked: true, + click: function () { + trayNotifications.enable(!trayNotifications.enabled(), notificationCallback); + userConfig.save(configPath); + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + goto: { + label: 'GoTo', + click: function () { + StartInterface("hifiapp:GOTO"); + pendingNotifications[HifiNotificationType.GOTO] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + people: { + label: 'People', + click: function () { + StartInterface("hifiapp:PEOPLE"); + pendingNotifications[HifiNotificationType.PEOPLE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + wallet: { + label: 'Wallet', + click: function () { + StartInterface("hifiapp:WALLET"); + pendingNotifications[HifiNotificationType.WALLET] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + marketplace: { + label: 'Market', + click: function () { + StartInterface("hifiapp:MARKET"); + pendingNotifications[HifiNotificationType.MARKETPLACE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, restart: { label: 'Start Server', click: function() { @@ -489,22 +498,36 @@ function buildMenuArray(serverState) { if (isShuttingDown) { menuArray.push(labels.shuttingDown); } else { - menuArray.push(labels.serverState); - menuArray.push(labels.version); - menuArray.push(separator); - menuArray.push(labels.goHome); - menuArray.push(separator); - menuArray.push(labels.restart); - menuArray.push(labels.stopServer); - menuArray.push(labels.settings); - menuArray.push(labels.viewLogs); - menuArray.push(separator); + if (isServerInstalled()) { + menuArray.push(labels.serverState); + menuArray.push(labels.version); + menuArray.push(separator); + } + if (isServerInstalled() && isInterfaceInstalled()) { + menuArray.push(labels.goHome); + menuArray.push(separator); + } + if (isServerInstalled()) { + menuArray.push(labels.restart); + menuArray.push(labels.stopServer); + menuArray.push(labels.settings); + menuArray.push(labels.viewLogs); + menuArray.push(separator); + } menuArray.push(labels.share); menuArray.push(separator); + if (isInterfaceInstalled()) { + menuArray.push(labels.goto); + menuArray.push(labels.people); + menuArray.push(labels.wallet); + menuArray.push(labels.marketplace); + menuArray.push(separator); + menuArray.push(labels.showNotifications); + menuArray.push(separator); + } menuArray.push(labels.quit); } - return menuArray; } @@ -528,6 +551,17 @@ function updateLabels(serverState) { labels.restart.label = "Restart Server"; labels.restart.enabled = false; } + + labels.showNotifications.checked = trayNotifications.enabled(); + labels.people.visible = trayNotifications.enabled(); + labels.goto.visible = trayNotifications.enabled(); + labels.wallet.visible = trayNotifications.enabled(); + labels.marketplace.visible = trayNotifications.enabled(); + labels.goto.icon = pendingNotifications[HifiNotificationType.GOTO] ? menuNotificationIcon : null; + labels.people.icon = pendingNotifications[HifiNotificationType.PEOPLE] ? menuNotificationIcon : null; + labels.wallet.icon = pendingNotifications[HifiNotificationType.WALLET] ? menuNotificationIcon : null; + labels.marketplace.icon = pendingNotifications[HifiNotificationType.MARKETPLACE] ? menuNotificationIcon : null; + } function updateTrayMenu(serverState) { @@ -807,7 +841,7 @@ function onContentLoaded() { deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); - if (dsPath && acPath) { + if (isServerInstalled()) { var dsArguments = ['--get-temp-name', '--parent-pid', process.pid]; domainServer = new Process('domain-server', dsPath, dsArguments, logPath); @@ -838,7 +872,7 @@ function onContentLoaded() { // If we were launched with the launchInterface option, then we need to launch interface now if (argv.launchInterface) { log.debug("Interface launch requested... argv.launchInterface:", argv.launchInterface); - startInterface(); + StartInterface(); } // If we were launched with the shutdownWith option, then we need to shutdown when that process (pid) @@ -869,7 +903,7 @@ app.on('ready', function() { // Create tray icon tray = new Tray(trayIcons[ProcessGroupStates.STOPPED]); - tray.setToolTip('High Fidelity Sandbox'); + tray.setToolTip('High Fidelity'); tray.on('click', function() { tray.popUpContextMenu(tray.menu); diff --git a/server-console/src/modules/hf-acctinfo.js b/server-console/src/modules/hf-acctinfo.js new file mode 100644 index 0000000000..828bc781b8 --- /dev/null +++ b/server-console/src/modules/hf-acctinfo.js @@ -0,0 +1,138 @@ +'use strict' + +const request = require('request'); +const extend = require('extend'); +const util = require('util'); +const events = require('events'); +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); + +const hfApp = require('./hf-app.js'); +const getInterfaceDataDirectory = hfApp.getInterfaceDataDirectory; + + +const VariantTypes = { + USER_TYPE: 1024 +} + +function AccountInfo() { + + var accountInfoPath = path.join(getInterfaceDataDirectory(), '/AccountInfo.bin'); + this.rawData = null; + this.parseOffset = 0; + try { + this.rawData = fs.readFileSync(accountInfoPath); + + this.data = this._parseMap(); + + } catch(e) { + console.log(e); + log.debug("AccountInfo file not found: " + accountInfoPath); + } +} + +AccountInfo.prototype = { + + accessToken: function (metaverseUrl) { + if (this.data && this.data[metaverseUrl] && this.data[metaverseUrl]["accessToken"]) { + return this.data[metaverseUrl]["accessToken"]["token"]; + } + return null; + }, + _parseUInt32: function () { + if (!this.rawData || (this.rawData.length - this.parseOffset < 4)) { + throw "Expected uint32"; + } + var result = this.rawData.readUInt32BE(this.parseOffset); + this.parseOffset += 4; + return result; + }, + _parseMap: function () { + var result = {}; + var n = this._parseUInt32(); + for (var i = 0; i < n; i++) { + var key = this._parseQString(); + result[key] = this._parseVariant(); + } + return result; + }, + _parseVariant: function () { + var varType = this._parseUInt32(); + var isNull = this.rawData[this.parseOffset++]; + + switch (varType) { + case VariantTypes.USER_TYPE: + //user type + var userTypeName = this._parseByteArray().toString('ascii').slice(0,-1); + if (userTypeName == "DataServerAccountInfo") { + return this._parseDataServerAccountInfo(); + } + else { + throw "Unknown custom type " + userTypeName; + } + break; + } + }, + _parseByteArray: function () { + var length = this._parseUInt32(); + if (length == 0xffffffff) { + return null; + } + var result = this.rawData.slice(this.parseOffset, this.parseOffset+length); + this.parseOffset += length; + return result; + + }, + _parseQString: function () { + if (!this.rawData || (this.rawData.length <= this.parseOffset)) { + throw "Expected QString"; + } + // length in bytes; + var length = this._parseUInt32(); + if (length == 0xFFFFFFFF) { + return null; + } + + if (this.rawData.length - this.parseOffset < length) { + throw "Insufficient buffer length for QString parsing"; + } + + // Convert from BE UTF16 to LE + var resultBuffer = this.rawData.slice(this.parseOffset, this.parseOffset+length); + resultBuffer.swap16(); + var result = resultBuffer.toString('utf16le'); + this.parseOffset += length; + return result; + }, + _parseDataServerAccountInfo: function () { + return { + accessToken: this._parseOAuthAccessToken(), + username: this._parseQString(), + xmppPassword: this._parseQString(), + discourseApiKey: this._parseQString(), + walletId: this._parseUUID(), + privateKey: this._parseByteArray(), + domainId: this._parseUUID(), + tempDomainId: this._parseUUID(), + tempDomainApiKey: this._parseQString() + + } + }, + _parseOAuthAccessToken: function () { + return { + token: this._parseQString(), + timestampHigh: this._parseUInt32(), + timestampLow: this._parseUInt32(), + tokenType: this._parseQString(), + refreshToken: this._parseQString() + } + }, + _parseUUID: function () { + this.parseOffset += 16; + return null; + } +} + +exports.AccountInfo = AccountInfo; \ No newline at end of file diff --git a/server-console/src/modules/hf-app.js b/server-console/src/modules/hf-app.js new file mode 100644 index 0000000000..625715b392 --- /dev/null +++ b/server-console/src/modules/hf-app.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const extend = require('extend'); +const Config = require('./config').Config +const os = require('os'); +const pathFinder = require('./path-finder'); +const path = require('path'); +const argv = require('yargs').argv; +const hfprocess = require('./hf-process'); +const osHomeDir = require('os-homedir'); +const Process = hfprocess.Process; + +const binaryType = argv.binaryType; +const osType = os.type(); + +exports.getBuildInfo = function() { + var buildInfoPath = null; + + if (osType == 'Windows_NT') { + buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); + } else if (osType == 'Darwin') { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); + } + } + + const DEFAULT_BUILD_INFO = { + releaseType: "", + buildIdentifier: "dev", + buildNumber: "0", + stableBuild: "0", + organization: "High Fidelity - dev", + appUserModelId: "com.highfidelity.sandbox-dev" + }; + var buildInfo = DEFAULT_BUILD_INFO; + + if (buildInfoPath) { + try { + buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); + } catch (e) { + buildInfo = DEFAULT_BUILD_INFO; + } + } + + return buildInfo; +} + +const buildInfo = exports.getBuildInfo(); +const interfacePath = pathFinder.discoveredPath("Interface", binaryType, buildInfo.releaseType); + +exports.startInterface = function(url) { + var argArray = []; + + // check if we have a url parameter to include + if (url) { + argArray = ["--url", url]; + } + console.log("Starting with " + url); + // create a new Interface instance - Interface makes sure only one is running at a time + var pInterface = new Process('Interface', interfacePath, argArray); + pInterface.detached = true; + pInterface.start(); +} + +exports.isInterfaceRunning = function(done) { + var pInterface = new Process('interface', 'interface.exe'); + return pInterface.isRunning(done); +} + + +exports.getRootHifiDataDirectory = function(local) { + var organization = buildInfo.organization; + if (osType == 'Windows_NT') { + if (local) { + return path.resolve(osHomeDir(), 'AppData/Local', organization); + } else { + return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + } + } else if (osType == 'Darwin') { + return path.resolve(osHomeDir(), 'Library/Application Support', organization); + } else { + return path.resolve(osHomeDir(), '.local/share/', organization); + } +} + +exports.getDomainServerClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/domain-server'); +} + +exports.getAssignmentClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/assignment-client'); +} + +exports.getApplicationDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Server Console'); +} + +exports.getInterfaceDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Interface'); +} \ No newline at end of file diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js new file mode 100644 index 0000000000..281ca1cb53 --- /dev/null +++ b/server-console/src/modules/hf-notifications.js @@ -0,0 +1,435 @@ +const request = require('request'); +const notifier = require('node-notifier'); +const os = require('os'); +const process = require('process'); +const hfApp = require('./hf-app'); +const path = require('path'); +const AccountInfo = require('./hf-acctinfo').AccountInfo; +const GetBuildInfo = hfApp.getBuildInfo; +const buildInfo = GetBuildInfo(); + +const notificationIcon = path.join(__dirname, '../../resources/console-notification.png'); +const STORIES_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const PEOPLE_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const WALLET_NOTIFICATION_POLL_TIME_MS = 600 * 1000; +const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; + +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' +const STORIES_URL= '/api/v1/user_stories'; +const USERS_URL= '/api/v1/users'; +const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; +const UPDATES_URL= '/api/v1/commerce/available_updates'; +const MAX_NOTIFICATION_ITEMS=30 +const STARTUP_MAX_NOTIFICATION_ITEMS=1 + + +const StartInterface=hfApp.startInterface; +const IsInterfaceRunning=hfApp.isInterfaceRunning; + +const NotificationType = { + GOTO: 'goto', + PEOPLE: 'people', + WALLET: 'wallet', + MARKETPLACE: 'marketplace' +}; + +function HifiNotification(notificationType, notificationData, menuNotificationCallback) { + this.type = notificationType; + this.data = notificationData; +} + +HifiNotification.prototype = { + show: function () { + var text = ""; + var message = ""; + var url = null; + var app = null; + switch (this.type) { + case NotificationType.GOTO: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = "You have " + this.data + " event invitation pending." + } else { + text = "You have " + this.data + " event invitations pending." + } + message = "Click to open GOTO."; + url="hifiapp:GOTO" + } else { + text = this.data.username + " " + this.data.action_string + " in " + this.data.place_name + "."; + message = "Click to go to " + this.data.place_name + "."; + url = "hifi://" + this.data.place_name + this.data.path; + } + break; + + case NotificationType.PEOPLE: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = this.data + " of your connections is online." + } else { + text = this.data + " of your connections are online." + } + message = "Click to open PEOPLE."; + url="hifiapp:PEOPLE" + } else { + text = this.data.username + " is available in " + this.data.location.root.name + "."; + message = "Click to join them."; + url="hifi://" + this.data.location.root.name + this.data.location.path; + } + break; + + case NotificationType.WALLET: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = "You have " + this.data + " unread Wallet transaction."; + } else { + text = "You have " + this.data + " unread Wallet transactions."; + } + message = "Click to open WALLET." + url = "hifiapp:hifi/commerce/wallet/Wallet.qml"; + break; + } + text = this.data.message.replace(/<\/?[^>]+(>|$)/g, ""); + message = "Click to open WALLET."; + url = "hifiapp:WALLET"; + break; + + case NotificationType.MARKETPLACE: + if (typeof(this.data) == "number") { + if (this.data == 1) { + text = this.data + " of your purchased items has an update available."; + } else { + text = this.data + " of your purchased items have updates available."; + } + } else { + text = "Update available for " + this.data.base_item_title + "."; + } + message = "Click to open MARKET."; + url = "hifiapp:MARKET"; + break; + } + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: message, + wait: true, + appID: buildInfo.appUserModelId, + url: url + }); + } +} + +function HifiNotifications(config, menuNotificationCallback) { + this.config = config; + this.menuNotificationCallback = menuNotificationCallback; + this.onlineUsers = new Set([]); + this.storiesSince = new Date(this.config.get("storiesNotifySince", "1970-01-01T00:00:00.000Z")); + this.peopleSince = new Date(this.config.get("peopleNotifySince", "1970-01-01T00:00:00.000Z")); + this.walletSince = new Date(this.config.get("walletNotifySince", "1970-01-01T00:00:00.000Z")); + this.marketplaceSince = new Date(this.config.get("marketplaceNotifySince", "1970-01-01T00:00:00.000Z")); + + this.enable(this.enabled()); + + var _menuNotificationCallback = menuNotificationCallback; + notifier.on('click', function (notifierObject, options) { + StartInterface(options.url); + _menuNotificationCallback(options.notificationType, false); + }); +} + +HifiNotifications.prototype = { + enable: function (enabled) { + this.config.set("enableTrayNotifications", enabled); + if (enabled) { + var _this = this; + this.storiesPollTimer = setInterval(function () { + var _since = _this.storiesSince; + _this.storiesSince = new Date(); + _this.pollForStories(_since); + }, + STORIES_NOTIFICATION_POLL_TIME_MS); + + this.peoplePollTimer = setInterval(function () { + var _since = _this.peopleSince; + _this.peopleSince = new Date(); + _this.pollForConnections(_since); + }, + PEOPLE_NOTIFICATION_POLL_TIME_MS); + + this.walletPollTimer = setInterval(function () { + var _since = _this.walletSince; + _this.walletSince = new Date(); + _this.pollForEconomicActivity(_since); + }, + WALLET_NOTIFICATION_POLL_TIME_MS); + + this.marketplacePollTimer = setInterval(function () { + var _since = _this.marketplaceSince; + _this.marketplaceSince = new Date(); + _this.pollForMarketplaceUpdates(_since); + }, + MARKETPLACE_NOTIFICATION_POLL_TIME_MS); + } else { + if (this.storiesPollTimer) { + clearInterval(this.storiesPollTimer); + } + if (this.peoplePollTimer) { + clearInterval(this.peoplePollTimer); + } + if (this.walletPollTimer) { + clearInterval(this.walletPollTimer); + } + if (this.marketplacePollTimer) { + clearInterval(this.marketplacePollTimer); + } + } + }, + enabled: function () { + return this.config.get("enableTrayNotifications", true); + }, + stopPolling: function () { + this.config.set("storiesNotifySince", this.storiesSince.toISOString()); + this.config.set("peopleNotifySince", this.peopleSince.toISOString()); + this.config.set("walletNotifySince", this.walletSince.toISOString()); + this.config.set("marketplaceNotifySince", this.marketplaceSince.toISOString()); + + this.enable(false); + }, + _pollToDisableHighlight: function (notifyType, error, data) { + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + if (!content.total_entries) { + this.menuNotificationCallback(notifyType, false); + } + }, + _pollCommon: function (notifyType, url, since, finished) { + + var _this = this; + IsInterfaceRunning(function (running) { + if (running) { + finished(false); + return; + } + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + + var maxNotificationItemCount = since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + finished(false); + return; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + finished(false); + return; + } + console.log(content); + if (!content.total_entries) { + finished(true, token); + return; + } + _this.menuNotificationCallback(notifyType, true); + if (content.total_entries >= maxNotificationItemCount) { + var notification = new HifiNotification(notifyType, content.total_entries); + notification.show(); + } else { + var notifyData = [] + switch (notifyType) { + case NotificationType.GOTO: + notifyData = content.user_stories; + break; + case NotificationType.PEOPLE: + notifyData = content.data.users; + break; + case NotificationType.WALLET: + notifyData = content.data.history; + break; + case NotificationType.MARKETPLACE: + notifyData = content.data.updates; + break; + } + + notifyData.forEach(function (notifyDataEntry) { + var notification = new HifiNotification(notifyType, notifyDataEntry); + notification.show(); + }); + } + finished(true, token); + }); + }); + }, + pollForStories: function (since) { + var _this = this; + var actions = 'announcement'; + var options = [ + 'since=' + since.getTime() / 1000, + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page='+MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for stories"); + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + console.log(url); + + _this._pollCommon(NotificationType.GOTO, + url, + since, + function (success, token) { + if (success) { + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page=1' + ]; + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + // call a second time to determine if there are no more stories and we should + // put out the light. + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + _this._pollToDisableHighlight(NotificationType.GOTO, error, data); + }); + } + }); + }, + pollForConnections: function (since) { + var _this = this; + var _since = since; + IsInterfaceRunning(function (running) { + if (running) { + return; + } + var options = [ + 'filter=connections', + 'status=online', + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for connections"); + var url = METAVERSE_SERVER_URL + USERS_URL + '?' + options.join('&'); + console.log(url); + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + // Users is a special case as we keep track of online users locally. + var maxNotificationItemCount = _since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + console.log(content); + if (!content.total_entries) { + _this.menuNotificationCallback(NotificationType.PEOPLE, false); + _this.onlineUsers = new Set([]); + return; + } + + var currentUsers = new Set([]); + var newUsers = new Set([]); + content.data.users.forEach(function (user) { + currentUsers.add(user.username); + if (!_this.onlineUsers.has(user.username)) { + newUsers.add(user); + _this.onlineUsers.add(user.username); + } + }); + _this.onlineUsers = currentUsers; + if (newUsers.size) { + _this.menuNotificationCallback(NotificationType.PEOPLE, true); + } + + if (newUsers.size >= maxNotificationItemCount) { + var notification = new HifiNotification(NotificationType.PEOPLE, newUsers.size); + notification.show(); + return; + } + newUsers.forEach(function (user) { + var notification = new HifiNotification(NotificationType.PEOPLE, user); + notification.show(); + }); + }); + }); + }, + pollForEconomicActivity: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + 1000 // total_entries is incorrect for wallet queries if results + // aren't all on one page, so grab them all on a single page + // for now. + ]; + console.log("Polling for economic activity"); + var url = METAVERSE_SERVER_URL + ECONOMIC_ACTIVITY_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.WALLET, url, since, function () {}); + }, + pollForMarketplaceUpdates: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for marketplace update"); + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.MARKETPLACE, url, since, function (success, token) { + if (success) { + var options = [ + 'page=1', + 'per_page=1' + ]; + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + _this._pollToDisableHighlight(NotificationType.MARKETPLACE, error, data); + }); + } + }); + } +}; + +exports.HifiNotifications = HifiNotifications; +exports.NotificationType = NotificationType; \ No newline at end of file diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js index 797ee38a0d..cf94ec6b29 100644 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -259,6 +259,24 @@ Process.prototype = extend(Process.prototype, { }; return logs; }, + isRunning: function (done) { + var _command = this.command; + if (os.type == 'Windows_NT') { + childProcess.exec('tasklist /FO CSV', function (err, stdout, stderr) { + var running = false; + stdout.split("\n").forEach(function (line) { + var exeData = line.split(","); + var executable = exeData[0].replace(/\"/g, "").toLowerCase(); + if (executable == _command) { + running = true; + } + }); + done(running); + }); + } else if (os.type == 'Darwin') { + console.log("TODO IsRunning Darwin"); + } + }, // Events onChildStartError: function(error) {