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 69f2445dd3..757007267f 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"; @@ -7643,6 +7648,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/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/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 f9b4c1a85d..4e20faead9 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) {