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 @@
+
+
+
+
\ 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) {