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
new file mode 100644
index 0000000000..8f391f24c0
--- /dev/null
+++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml
@@ -0,0 +1,290 @@
+//
+// marketplaceItemTester
+// qml/hifi/commerce/marketplaceItemTester
+//
+// Load items not in the marketplace for testing purposes
+//
+// Created by Zach Fox on 2018-09-05
+// Copyright 2018 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+import QtQuick 2.5
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+import QtQuick.Dialogs 1.0
+import QtQuick.Layouts 1.1
+import Hifi 1.0 as Hifi
+import "../../../styles-uit" as HifiStylesUit
+import "../../../controls-uit" as HifiControlsUit
+
+
+
+Rectangle {
+ id: root
+
+ property string installedApps
+ property var nextResourceObjectId: 0
+ signal sendToScript(var message)
+
+ HifiStylesUit.HifiConstants { id: hifi }
+ ListModel { id: resourceListModel }
+
+ color: hifi.colors.white
+
+ AnimatedImage {
+ id: spinner;
+ source: "spinner.gif"
+ width: 74;
+ height: width;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.horizontalCenter: parent.horizontalCenter;
+ }
+
+ function fromScript(message) {
+ switch (message.method) {
+ case "newResourceObjectInTest":
+ var resourceObject = message.resourceObject;
+ resourceListModel.append(resourceObject);
+ spinner.visible = false;
+ break;
+ case "nextObjectIdInTest":
+ nextResourceObjectId = message.id;
+ spinner.visible = false;
+ break;
+ }
+ }
+
+ function buildResourceObj(resource) {
+ resource = resource.trim();
+ var assetType = (resource.match(/\.app\.json$/) ? "application" :
+ resource.match(/\.fst$/) ? "avatar" :
+ resource.match(/\.json\.gz$/) ? "content set" :
+ resource.match(/\.json$/) ? "entity or wearable" :
+ "unknown");
+ return { "id": nextResourceObjectId++,
+ "resource": resource,
+ "assetType": assetType };
+ }
+
+ function installResourceObj(resourceObj) {
+ if ("application" === resourceObj.assetType) {
+ Commerce.installApp(resourceObj.resource);
+ }
+ }
+
+ function addAllInstalledAppsToList() {
+ var i, apps = Commerce.getInstalledApps().split(","), len = apps.length;
+ for(i = 0; i < len - 1; ++i) {
+ if (i in apps) {
+ resourceListModel.append(buildResourceObj(apps[i]));
+ }
+ }
+ }
+
+ function toUrl(resource) {
+ var httpPattern = /^http/i;
+ return httpPattern.test(resource) ? resource : "file:///" + resource;
+ }
+
+ function rezEntity(resource, entityType) {
+ sendToScript({
+ method: 'tester_rezClicked',
+ itemHref: toUrl(resource),
+ itemType: entityType});
+ }
+
+ ListView {
+ anchors.fill: parent
+ anchors.leftMargin: 12
+ anchors.bottomMargin: 40
+ anchors.rightMargin: 12
+ model: resourceListModel
+ spacing: 5
+ interactive: false
+
+ delegate: RowLayout {
+ anchors.left: parent.left
+ width: parent.width
+ spacing: 5
+
+ property var actions: {
+ "forward": function(resource, assetType){
+ switch(assetType) {
+ case "application":
+ Commerce.openApp(resource);
+ break;
+ case "avatar":
+ MyAvatar.useFullAvatarURL(resource);
+ break;
+ case "content set":
+ urlHandler.handleUrl("hifi://localhost/0,0,0");
+ Commerce.replaceContentSet(toUrl(resource), "");
+ break;
+ case "entity":
+ case "wearable":
+ rezEntity(resource, assetType);
+ break;
+ default:
+ print("Marketplace item tester unsupported assetType " + assetType);
+ }
+ },
+ "trash": function(){
+ if ("application" === assetType) {
+ Commerce.uninstallApp(resource);
+ }
+ sendToScript({
+ method: "tester_deleteResourceObject",
+ objectId: resourceListModel.get(index).id});
+ resourceListModel.remove(index);
+ }
+ }
+
+ Column {
+ Layout.preferredWidth: root.width * .6
+ spacing: 5
+ Text {
+ text: {
+ var match = resource.match(/\/([^/]*)$/);
+ return match ? match[1] : resource;
+ }
+ font.pointSize: 12
+ horizontalAlignment: Text.AlignBottom
+ }
+ Text {
+ text: resource
+ font.pointSize: 8
+ width: root.width * .6
+ horizontalAlignment: Text.AlignBottom
+ wrapMode: Text.WrapAnywhere
+ }
+ }
+
+ ComboBox {
+ id: comboBox
+
+ Layout.preferredWidth: root.width * .2
+
+ model: [
+ "application",
+ "avatar",
+ "content set",
+ "entity",
+ "wearable",
+ "unknown"
+ ]
+
+ currentIndex: (("entity or wearable" === assetType) ?
+ model.indexOf("unknown") : model.indexOf(assetType))
+
+ Component.onCompleted: {
+ onCurrentIndexChanged.connect(function() {
+ assetType = model[currentIndex];
+ sendToScript({
+ method: "tester_updateResourceObjectAssetType",
+ objectId: resourceListModel.get(index)["id"],
+ assetType: assetType });
+ });
+ }
+ }
+
+ Repeater {
+ model: [ "forward", "trash" ]
+
+ HifiStylesUit.HiFiGlyphs {
+ property var glyphs: {
+ "application": hifi.glyphs.install,
+ "avatar": hifi.glyphs.avatar,
+ "content set": hifi.glyphs.globe,
+ "entity": hifi.glyphs.wand,
+ "trash": hifi.glyphs.trash,
+ "unknown": hifi.glyphs.circleSlash,
+ "wearable": hifi.glyphs.hat,
+ }
+ text: (("trash" === modelData) ?
+ glyphs.trash :
+ glyphs[comboBox.model[comboBox.currentIndex]])
+ size: ("trash" === modelData) ? 22 : 30
+ color: hifi.colors.black
+ horizontalAlignment: Text.AlignHCenter
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ actions[modelData](resource, comboBox.currentText);
+ }
+ }
+ }
+ }
+ }
+
+ headerPositioning: ListView.OverlayHeader
+ header: HifiStylesUit.RalewayRegular {
+ id: rootHeader
+ text: "Marketplace Item Tester"
+ height: 80
+ width: paintedWidth
+ size: 22
+ color: hifi.colors.black
+ anchors.left: parent.left
+ anchors.leftMargin: 12
+ }
+
+ footerPositioning: ListView.OverlayFooter
+ footer: Row {
+ id: rootActions
+ spacing: 20
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ property string currentAction
+ property var actions: {
+ "Load File": function(){
+ rootActions.currentAction = "load file";
+ Window.browseChanged.connect(onResourceSelected);
+ Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)");
+ },
+ "Load URL": function(){
+ rootActions.currentAction = "load url";
+ Window.promptTextChanged.connect(onResourceSelected);
+ Window.promptAsync("Please enter a URL", "");
+ }
+ }
+
+ function onResourceSelected(resource) {
+ // It is possible that we received the present signal
+ // from something other than our browserAsync window.
+ // Alas, there is nothing we can do about that so charge
+ // ahead as though we are sure the present signal is one
+ // we expect.
+ switch(currentAction) {
+ case "load file":
+ Window.browseChanged.disconnect(onResourceSelected);
+ break
+ case "load url":
+ Window.promptTextChanged.disconnect(onResourceSelected);
+ break;
+ }
+ if (resource) {
+ var resourceObj = buildResourceObj(resource);
+ installResourceObj(resourceObj);
+ sendToScript({
+ method: 'tester_newResourceObject',
+ resourceObject: resourceObj });
+ }
+ }
+
+ Repeater {
+ model: [ "Load File", "Load URL" ]
+ HifiControlsUit.Button {
+ color: hifi.buttons.blue
+ fontSize: 20
+ text: modelData
+ width: root.width / 3
+ height: 40
+ onClicked: actions[text]()
+ }
+ }
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif
new file mode 100644
index 0000000000..00f75ae62f
Binary files /dev/null and b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif differ
diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
index 032d9b0199..eeb9ac3c54 100644
--- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
@@ -59,7 +59,7 @@ Item {
Connections {
target: Commerce;
-
+
onContentSetChanged: {
if (contentSetHref === root.itemHref) {
showConfirmation = true;
@@ -135,7 +135,7 @@ Item {
anchors.topMargin: 8;
width: 30;
height: width;
-
+
HiFiGlyphs {
id: closeContextMenuGlyph;
text: hifi.glyphs.close;
@@ -376,7 +376,7 @@ Item {
}
}
}
-
+
transform: Rotation {
id: rotation;
origin.x: flipable.width/2;
@@ -509,7 +509,7 @@ Item {
}
verticalAlignment: Text.AlignTop;
}
-
+
HiFiGlyphs {
id: statusIcon;
text: {
@@ -588,7 +588,7 @@ Item {
border.width: 1;
border.color: "#E2334D";
}
-
+
HiFiGlyphs {
id: contextMenuGlyph;
text: hifi.glyphs.verticalEllipsis;
@@ -615,7 +615,7 @@ Item {
}
}
}
-
+
Rectangle {
id: rezzedNotifContainer;
z: 998;
@@ -663,13 +663,13 @@ Item {
Tablet.playSound(TabletEnums.ButtonHover);
}
}
-
+
onFocusChanged: {
if (focus) {
Tablet.playSound(TabletEnums.ButtonHover);
}
}
-
+
onClicked: {
Tablet.playSound(TabletEnums.ButtonClick);
if (root.itemType === "contentSet") {
@@ -775,7 +775,7 @@ Item {
// Style
color: hifi.colors.redAccent;
horizontalAlignment: Text.AlignRight;
-
+
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
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 2ae5dff573..f23410bff9 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -1691,21 +1691,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
return DependencyManager::get()->navigationFocused() ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float {
-#if defined(Q_OS_WIN)
+#if defined(Q_OS_WIN)
return 1;
#else
return 0;
#endif
});
_applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float {
-#if defined(Q_OS_MAC)
+#if defined(Q_OS_MAC)
return 1;
#else
return 0;
#endif
});
_applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float {
-#if defined(Q_OS_ANDROID)
+#if defined(Q_OS_ANDROID)
return 1;
#else
return 0;
@@ -1759,10 +1759,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Make sure we don't time out during slow operations at startup
updateHeartbeat();
-
QTimer* settingsTimer = new QTimer();
moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{
- connect(qApp, &Application::beforeAboutToQuit, [this, settingsTimer]{
+ // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the
+ // receiver object, otherwise it will run on the application thread and trigger a warning
+ // about trying to kill the timer on the main thread.
+ connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{
// Disconnect the signal from the save settings
QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings);
// Stop the settings timer
@@ -2881,9 +2883,10 @@ void Application::initializeUi() {
QUrl{ "hifi/commerce/common/CommerceLightbox.qml" },
QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" },
QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" },
- QUrl{ "hifi/commerce/common/SortableListModel.qml" },
QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" },
+ QUrl{ "hifi/commerce/common/SortableListModel.qml" },
QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" },
+ QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"},
QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" },
QUrl{ "hifi/commerce/purchases/Purchases.qml" },
QUrl{ "hifi/commerce/wallet/Help.qml" },
@@ -3503,13 +3506,14 @@ bool Application::isServerlessMode() const {
}
void Application::setIsInterstitialMode(bool interstitialMode) {
- Settings settings;
- bool enableInterstitial = settings.value("enableIntersitialMode", false).toBool();
- if (_interstitialMode != interstitialMode && enableInterstitial) {
- _interstitialMode = interstitialMode;
+ bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled();
+ if (enableInterstitial) {
+ if (_interstitialMode != interstitialMode) {
+ _interstitialMode = interstitialMode;
- DependencyManager::get()->setAudioPaused(_interstitialMode);
- DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode);
+ DependencyManager::get()->setAudioPaused(_interstitialMode);
+ DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode);
+ }
}
}
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 3bebc60480..eedbdb7622 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -432,7 +432,7 @@ public slots:
void setIsServerlessMode(bool serverlessDomain);
void loadServerlessDomain(QUrl domainURL, bool errorDomain = false);
- void setIsInterstitialMode(bool interstialMode);
+ void setIsInterstitialMode(bool interstitialMode);
void updateVerboseLogging();
diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp
index 3c85cfb339..e86061b090 100644
--- a/interface/src/ConnectionMonitor.cpp
+++ b/interface/src/ConnectionMonitor.cpp
@@ -41,9 +41,15 @@ void ConnectionMonitor::init() {
}
connect(&_timer, &QTimer::timeout, this, [this]() {
- qDebug() << "ConnectionMonitor: Redirecting to 404 error domain";
// set in a timeout error
- emit setRedirectErrorState(REDIRECT_HIFI_ADDRESS, 5);
+ bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled();
+ if (enableInterstitial) {
+ qDebug() << "ConnectionMonitor: Redirecting to 404 error domain";
+ emit setRedirectErrorState(REDIRECT_HIFI_ADDRESS, "", 5);
+ } else {
+ qDebug() << "ConnectionMonitor: Showing connection failure window";
+ DependencyManager::get()->setDomainConnectionFailureVisibility(true);
+ }
});
}
@@ -53,4 +59,8 @@ void ConnectionMonitor::startTimer() {
void ConnectionMonitor::stopTimer() {
_timer.stop();
+ bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled();
+ if (!enableInterstitial) {
+ DependencyManager::get()->setDomainConnectionFailureVisibility(false);
+ }
}
diff --git a/interface/src/ConnectionMonitor.h b/interface/src/ConnectionMonitor.h
index 5e75e2618b..2fda6ef7cd 100644
--- a/interface/src/ConnectionMonitor.h
+++ b/interface/src/ConnectionMonitor.h
@@ -24,7 +24,7 @@ public:
void init();
signals:
- void setRedirectErrorState(QUrl errorURL, int reasonCode);
+ void setRedirectErrorState(QUrl errorURL, QString reasonMessage = "", int reasonCode = -1, const QString& extraInfo = "");
private slots:
void startTimer();
@@ -34,4 +34,4 @@ private:
QTimer _timer;
};
-#endif // hifi_ConnectionMonitor_h
\ No newline at end of file
+#endif // hifi_ConnectionMonitor_h
diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp
index 1faf17ea9a..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();
}
@@ -456,31 +458,37 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
}
void AvatarManager::clearOtherAvatars() {
- // Remove other avatars from the world but don't actually remove them from _avatarHash
- // each will either be removed on timeout or will re-added to the world on receipt of update.
- const render::ScenePointer& scene = qApp->getMain3DScene();
- render::Transaction transaction;
-
- QReadLocker locker(&_hashLock);
- AvatarHash::iterator avatarIterator = _avatarHash.begin();
- while (avatarIterator != _avatarHash.end()) {
- auto avatar = std::static_pointer_cast(avatarIterator.value());
- if (avatar != _myAvatar) {
- handleRemovedAvatar(avatar);
- avatarIterator = _avatarHash.erase(avatarIterator);
- } else {
- ++avatarIterator;
- }
- }
- assert(scene);
- scene->enqueueTransaction(transaction);
_myAvatar->clearLookAtTargetAvatar();
+
+ // setup a vector of removed avatars outside the scope of the hash lock
+ std::vector removedAvatars;
+
+ {
+ QWriteLocker locker(&_hashLock);
+
+ removedAvatars.reserve(_avatarHash.size());
+
+ auto avatarIterator = _avatarHash.begin();
+ while (avatarIterator != _avatarHash.end()) {
+ auto avatar = std::static_pointer_cast(avatarIterator.value());
+ if (avatar != _myAvatar) {
+ removedAvatars.push_back(avatar);
+ avatarIterator = _avatarHash.erase(avatarIterator);
+ } else {
+ ++avatarIterator;
+ }
+ }
+ }
+
+ for (auto& av : removedAvatars) {
+ handleRemovedAvatar(av);
+ }
}
void AvatarManager::deleteAllAvatars() {
assert(_avatarsToChangeInPhysics.empty());
- QReadLocker locker(&_hashLock);
+ QWriteLocker locker(&_hashLock);
AvatarHash::iterator avatarIterator = _avatarHash.begin();
while (avatarIterator != _avatarHash.end()) {
auto avatar = std::static_pointer_cast(avatarIterator.value());
diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h
index 306ba6f39b..9c4287728d 100644
--- a/interface/src/avatar/AvatarManager.h
+++ b/interface/src/avatar/AvatarManager.h
@@ -204,7 +204,12 @@ private:
void simulateAvatarFades(float deltaTime);
AvatarSharedPointer newSharedAvatar() override;
- void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
+
+ // called only from the AvatarHashMap thread - cannot be called while this thread holds the
+ // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree
+ // frequently grabs a read lock on the hash to get a given avatar by ID
+ void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar,
+ KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
QVector _avatarsToFade;
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 7d7f41ad0e..aa39fdc1b9 100644
--- a/interface/src/commerce/QmlCommerce.cpp
+++ b/interface/src/commerce/QmlCommerce.cpp
@@ -247,12 +247,18 @@ void QmlCommerce::transferAssetToUsername(const QString& username,
}
void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& certificateID) {
- auto ledger = DependencyManager::get();
- ledger->updateLocation(certificateID, DependencyManager::get()->getPlaceName(), true);
+ if (!certificateID.isEmpty()) {
+ auto ledger = DependencyManager::get();
+ ledger->updateLocation(
+ certificateID,
+ DependencyManager::get()->getPlaceName(),
+ true);
+ }
qApp->replaceDomainContent(itemHref);
- QJsonObject messageProperties = { { "status", "SuccessfulRequestToReplaceContent" }, { "content_set_url", itemHref } };
+ QJsonObject messageProperties = {
+ { "status", "SuccessfulRequestToReplaceContent" },
+ { "content_set_url", itemHref } };
UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties);
-
emit contentSetChanged(itemHref);
}
@@ -276,6 +282,7 @@ QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) {
// Thus, we protect against deleting the .app.json from the user's disk (below)
// by skipping that check for the app we just installed.
if ((justInstalledAppID != "") && ((justInstalledAppID + ".app.json") == appFileName)) {
+ installedAppsFromMarketplace += appFileName + ",";
continue;
}
diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp
index 5fbe3a90b5..3c66923b4e 100644
--- a/interface/src/raypick/LaserPointer.cpp
+++ b/interface/src/raypick/LaserPointer.cpp
@@ -35,6 +35,14 @@ void LaserPointer::editRenderStatePath(const std::string& state, const QVariant&
}
}
+PickResultPointer LaserPointer::getPickResultCopy(const PickResultPointer& pickResult) const {
+ auto rayPickResult = std::dynamic_pointer_cast(pickResult);
+ if (!rayPickResult) {
+ return std::make_shared();
+ }
+ return std::make_shared(*rayPickResult.get());
+}
+
QVariantMap LaserPointer::toVariantMap() const {
QVariantMap qVariantMap;
diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h
index c0ac3259d9..b391f60f85 100644
--- a/interface/src/raypick/LaserPointer.h
+++ b/interface/src/raypick/LaserPointer.h
@@ -47,6 +47,8 @@ public:
static std::shared_ptr buildRenderState(const QVariantMap& propMap);
protected:
+ PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override;
+
void editRenderStatePath(const std::string& state, const QVariant& pathProps) override;
glm::vec3 getPickOrigin(const PickResultPointer& pickResult) const override;
diff --git a/interface/src/raypick/ParabolaPointer.cpp b/interface/src/raypick/ParabolaPointer.cpp
index ad698c409b..33fa8738d9 100644
--- a/interface/src/raypick/ParabolaPointer.cpp
+++ b/interface/src/raypick/ParabolaPointer.cpp
@@ -30,6 +30,14 @@ ParabolaPointer::ParabolaPointer(const QVariant& rayProps, const RenderStateMap&
{
}
+PickResultPointer ParabolaPointer::getPickResultCopy(const PickResultPointer& pickResult) const {
+ auto parabolaPickResult = std::dynamic_pointer_cast(pickResult);
+ if (!parabolaPickResult) {
+ return std::make_shared();
+ }
+ return std::make_shared(*parabolaPickResult.get());
+}
+
void ParabolaPointer::editRenderStatePath(const std::string& state, const QVariant& pathProps) {
auto renderState = std::static_pointer_cast(_renderStates[state]);
if (renderState) {
diff --git a/interface/src/raypick/ParabolaPointer.h b/interface/src/raypick/ParabolaPointer.h
index 526abe3b0d..8fb864c07b 100644
--- a/interface/src/raypick/ParabolaPointer.h
+++ b/interface/src/raypick/ParabolaPointer.h
@@ -102,6 +102,8 @@ public:
static std::shared_ptr buildRenderState(const QVariantMap& propMap);
protected:
+ virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override;
+
void editRenderStatePath(const std::string& state, const QVariant& pathProps) override;
glm::vec3 getPickOrigin(const PickResultPointer& pickResult) const override;
diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp
index 06e3e52d21..b648e125bf 100644
--- a/interface/src/raypick/StylusPointer.cpp
+++ b/interface/src/raypick/StylusPointer.cpp
@@ -147,6 +147,14 @@ bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) {
return false;
}
+PickResultPointer StylusPointer::getPickResultCopy(const PickResultPointer& pickResult) const {
+ auto stylusPickResult = std::dynamic_pointer_cast(pickResult);
+ if (!stylusPickResult) {
+ return std::make_shared();
+ }
+ return std::make_shared(*stylusPickResult.get());
+}
+
Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& pickResult) {
auto stylusPickResult = std::static_pointer_cast(pickResult);
if (!stylusPickResult) {
diff --git a/interface/src/raypick/StylusPointer.h b/interface/src/raypick/StylusPointer.h
index 4095acb529..ff60fd78e5 100644
--- a/interface/src/raypick/StylusPointer.h
+++ b/interface/src/raypick/StylusPointer.h
@@ -42,6 +42,7 @@ protected:
Buttons getPressedButtons(const PickResultPointer& pickResult) override;
bool shouldHover(const PickResultPointer& pickResult) override;
bool shouldTrigger(const PickResultPointer& pickResult) override;
+ virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override;
PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override;
diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp
index e3ae65aee1..d4eb37e0aa 100644
--- a/interface/src/scripting/WindowScriptingInterface.cpp
+++ b/interface/src/scripting/WindowScriptingInterface.cpp
@@ -180,6 +180,14 @@ void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& loc
Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location);
}
+bool WindowScriptingInterface::getInterstitialModeEnabled() const {
+ return DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled();
+}
+
+void WindowScriptingInterface::setInterstitialModeEnabled(bool enableInterstitialMode) {
+ DependencyManager::get()->getDomainHandler().setInterstitialModeEnabled(enableInterstitialMode);
+}
+
bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) {
auto offscreenUi = DependencyManager::get();
return offscreenUi->isPointOnDesktopWindow(point);
diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h
index 3827406729..ddd7159f23 100644
--- a/interface/src/scripting/WindowScriptingInterface.h
+++ b/interface/src/scripting/WindowScriptingInterface.h
@@ -49,6 +49,7 @@ class WindowScriptingInterface : public QObject, public Dependency {
Q_PROPERTY(int innerHeight READ getInnerHeight)
Q_PROPERTY(int x READ getX)
Q_PROPERTY(int y READ getY)
+ Q_PROPERTY(bool interstitialModeEnabled READ getInterstitialModeEnabled WRITE setInterstitialModeEnabled)
public:
WindowScriptingInterface();
@@ -758,6 +759,9 @@ private:
QString getPreviousBrowseAssetLocation() const;
void setPreviousBrowseAssetLocation(const QString& location);
+ bool getInterstitialModeEnabled() const;
+ void setInterstitialModeEnabled(bool enableInterstitialMode);
+
void ensureReticleVisible() const;
int createMessageBox(QString title, QString text, int buttons, int defaultButton);
diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp
index 3c84b7ce1f..d00bc29054 100644
--- a/libraries/audio-client/src/AudioClient.cpp
+++ b/libraries/audio-client/src/AudioClient.cpp
@@ -100,6 +100,13 @@ QList getAvailableDevices(QAudio::Mode mode) {
// now called from a background thread, to keep blocking operations off the audio thread
void AudioClient::checkDevices() {
+ // Make sure we're not shutting down
+ Lock timerMutex(_checkDevicesMutex);
+ // If we HAVE shut down after we were queued, but prior to execution, early exit
+ if (nullptr == _checkDevicesTimer) {
+ return;
+ }
+
auto inputDevices = getAvailableDevices(QAudio::AudioInput);
auto outputDevices = getAvailableDevices(QAudio::AudioOutput);
@@ -278,9 +285,6 @@ void AudioClient::customDeleter() {
_shouldRestartInputSetup = false;
#endif
stop();
- _checkDevicesTimer->stop();
- _checkPeakValuesTimer->stop();
-
deleteLater();
}
@@ -653,12 +657,26 @@ void AudioClient::start() {
}
void AudioClient::stop() {
-
qCDebug(audioclient) << "AudioClient::stop(), requesting switchInputToAudioDevice() to shut down";
switchInputToAudioDevice(QAudioDeviceInfo(), true);
qCDebug(audioclient) << "AudioClient::stop(), requesting switchOutputToAudioDevice() to shut down";
switchOutputToAudioDevice(QAudioDeviceInfo(), true);
+
+ // Stop triggering the checks
+ QObject::disconnect(_checkPeakValuesTimer, &QTimer::timeout, nullptr, nullptr);
+ QObject::disconnect(_checkDevicesTimer, &QTimer::timeout, nullptr, nullptr);
+
+ // Destruction of the pointers will occur when the parent object (this) is destroyed)
+ {
+ Lock lock(_checkDevicesMutex);
+ _checkDevicesTimer = nullptr;
+ }
+ {
+ Lock lock(_checkPeakValuesMutex);
+ _checkPeakValuesTimer = nullptr;
+ }
+
#if defined(Q_OS_ANDROID)
_checkInputTimer.stop();
disconnect(&_checkInputTimer, &QTimer::timeout, 0, 0);
diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h
index 499397b385..5e7f1fb8a0 100644
--- a/libraries/audio-client/src/AudioClient.h
+++ b/libraries/audio-client/src/AudioClient.h
@@ -447,7 +447,9 @@ private:
bool _shouldRestartInputSetup { true }; // Should we restart the input device because of an unintended stop?
#endif
+ Mutex _checkDevicesMutex;
QTimer* _checkDevicesTimer { nullptr };
+ Mutex _checkPeakValuesMutex;
QTimer* _checkPeakValuesTimer { nullptr };
bool _isRecording { false };
diff --git a/libraries/audio-client/src/AudioPeakValues.cpp b/libraries/audio-client/src/AudioPeakValues.cpp
index 0b8921a117..a50567da7f 100644
--- a/libraries/audio-client/src/AudioPeakValues.cpp
+++ b/libraries/audio-client/src/AudioPeakValues.cpp
@@ -40,6 +40,12 @@ void release(IAudioClient* audioClient) {
}
void AudioClient::checkPeakValues() {
+ // Guard against running during shutdown
+ Lock timerMutex(_checkPeakValuesMutex);
+ if (nullptr == _checkPeakValuesTimer) {
+ return;
+ }
+
// prepare the windows environment
CoInitialize(NULL);
diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp
index 67f9952771..da284f19a3 100644
--- a/libraries/audio/src/Sound.cpp
+++ b/libraries/audio/src/Sound.cpp
@@ -43,8 +43,11 @@ void soundSharedPointerFromScriptValue(const QScriptValue& object, SharedSoundPo
}
}
-SoundScriptingInterface::SoundScriptingInterface(SharedSoundPointer sound) : _sound(sound) {
- QObject::connect(sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready);
+SoundScriptingInterface::SoundScriptingInterface(const SharedSoundPointer& sound) : _sound(sound) {
+ // During shutdown we can sometimes get an empty sound pointer back
+ if (_sound) {
+ QObject::connect(_sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready);
+ }
}
Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) :
diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h
index 348600e4ae..a0544870d0 100644
--- a/libraries/audio/src/Sound.h
+++ b/libraries/audio/src/Sound.h
@@ -105,11 +105,11 @@ class SoundScriptingInterface : public QObject {
Q_PROPERTY(float duration READ getDuration)
public:
- SoundScriptingInterface(SharedSoundPointer sound);
- SharedSoundPointer getSound() { return _sound; }
+ SoundScriptingInterface(const SharedSoundPointer& sound);
+ const SharedSoundPointer& getSound() { return _sound; }
- bool isReady() const { return _sound->isReady(); }
- float getDuration() { return _sound->getDuration(); }
+ bool isReady() const { return _sound ? _sound->isReady() : false; }
+ float getDuration() { return _sound ? _sound->getDuration() : 0.0f; }
/**jsdoc
* Triggered when the sound has been downloaded and is ready to be played.
diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp
index d205a915f8..01557e307e 100644
--- a/libraries/avatars/src/AvatarHashMap.cpp
+++ b/libraries/avatars/src/AvatarHashMap.cpp
@@ -66,6 +66,22 @@ void AvatarReplicas::removeReplicas(const QUuid& parentID) {
}
}
+std::vector AvatarReplicas::takeReplicas(const QUuid& parentID) {
+ std::vector replicas;
+
+ auto it = _replicasMap.find(parentID);
+
+ if (it != _replicasMap.end()) {
+ // take a copy of the replica shared pointers for this parent
+ replicas.swap(it->second);
+
+ // erase the replicas for this parent from our map
+ _replicasMap.erase(it);
+ }
+
+ return replicas;
+}
+
void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) {
if (_replicasMap.find(parentID) != _replicasMap.end()) {
auto &replicas = _replicasMap[parentID];
@@ -386,24 +402,31 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S
}
void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) {
- QWriteLocker locker(&_hashLock);
+ std::vector removedAvatars;
- auto replicaIDs = _replicas.getReplicaIDs(sessionUUID);
- _replicas.removeReplicas(sessionUUID);
- for (auto id : replicaIDs) {
- auto removedReplica = _avatarHash.take(id);
- if (removedReplica) {
- handleRemovedAvatar(removedReplica, removalReason);
+ {
+ QWriteLocker locker(&_hashLock);
+
+ auto replicas = _replicas.takeReplicas(sessionUUID);
+
+ for (auto& replica : replicas) {
+ auto removedReplica = _avatarHash.take(replica->getID());
+ if (removedReplica) {
+ removedAvatars.push_back(removedReplica);
+ }
+ }
+
+ _pendingAvatars.remove(sessionUUID);
+ auto removedAvatar = _avatarHash.take(sessionUUID);
+
+ if (removedAvatar) {
+ removedAvatars.push_back(removedAvatar);
}
}
- _pendingAvatars.remove(sessionUUID);
- auto removedAvatar = _avatarHash.take(sessionUUID);
-
- if (removedAvatar) {
+ for (auto& removedAvatar: removedAvatars) {
handleRemovedAvatar(removedAvatar, removalReason);
}
-
}
void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
@@ -421,11 +444,18 @@ void AvatarHashMap::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& ol
}
void AvatarHashMap::clearOtherAvatars() {
- QWriteLocker locker(&_hashLock);
+ QList removedAvatars;
- for (auto& av : _avatarHash) {
- handleRemovedAvatar(av);
+ {
+ QWriteLocker locker(&_hashLock);
+
+ // grab a copy of the current avatars so we can call handleRemoveAvatar for them
+ removedAvatars = _avatarHash.values();
+
+ _avatarHash.clear();
}
- _avatarHash.clear();
+ for (auto& av : removedAvatars) {
+ handleRemovedAvatar(av);
+ }
}
diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h
index 70d7f8c04d..c2cb448e52 100644
--- a/libraries/avatars/src/AvatarHashMap.h
+++ b/libraries/avatars/src/AvatarHashMap.h
@@ -49,6 +49,7 @@ public:
void parseDataFromBuffer(const QUuid& parentID, const QByteArray& buffer);
void processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged);
void removeReplicas(const QUuid& parentID);
+ std::vector takeReplicas(const QUuid& parentID);
void processTrait(const QUuid& parentID, AvatarTraits::TraitType traitType, QByteArray traitBinaryData);
void processDeletedTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID);
void processTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType,
@@ -179,7 +180,7 @@ protected:
bool& isNew);
virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID) const; // uses a QReadLocker on the hashLock
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason);
-
+
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason);
AvatarHash _avatarHash;
diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h
index c7cdf8f4ea..17041a5fd7 100644
--- a/libraries/networking/src/AddressManager.h
+++ b/libraries/networking/src/AddressManager.h
@@ -140,8 +140,7 @@ public:
*
* @typedef {number} location.LookupTrigger
*/
- enum LookupTrigger
- {
+ enum LookupTrigger {
UserInput,
Back,
Forward,
@@ -207,9 +206,8 @@ public slots:
// functions and signals that should be exposed are moved to a scripting interface class.
//
// we currently expect this to be called from NodeList once handleLookupString has been called with a path
- bool goToViewpointForPath(const QString& viewpointString, const QString& pathString) {
- return handleViewpoint(viewpointString, false, DomainPathResponse, false, pathString);
- }
+ bool goToViewpointForPath(const QString& viewpointString, const QString& pathString)
+ { return handleViewpoint(viewpointString, false, DomainPathResponse, false, pathString); }
/**jsdoc
* Go back to the previous location in your navigation history, if there is one.
@@ -231,8 +229,7 @@ public slots:
* location history is correctly maintained.
*/
void goToLocalSandbox(QString path = "", LookupTrigger trigger = LookupTrigger::StartupFromSettings) {
- handleUrl(SANDBOX_HIFI_ADDRESS + path, trigger);
- }
+ handleUrl(SANDBOX_HIFI_ADDRESS + path, trigger); }
/**jsdoc
* Go to the default "welcome" metaverse address.
@@ -364,8 +361,7 @@ signals:
* location.locationChangeRequired.connect(onLocationChangeRequired);
*/
void locationChangeRequired(const glm::vec3& newPosition,
- bool hasOrientationChange,
- const glm::quat& newOrientation,
+ bool hasOrientationChange, const glm::quat& newOrientation,
bool shouldFaceLocation);
/**jsdoc
@@ -448,11 +444,8 @@ private:
bool handleNetworkAddress(const QString& lookupString, LookupTrigger trigger, bool& hostChanged);
void handlePath(const QString& path, LookupTrigger trigger, bool wasPathOnly = false);
- bool handleViewpoint(const QString& viewpointString,
- bool shouldFace,
- LookupTrigger trigger,
- bool definitelyPathOnly = false,
- const QString& pathString = QString());
+ bool handleViewpoint(const QString& viewpointString, bool shouldFace, LookupTrigger trigger,
+ bool definitelyPathOnly = false, const QString& pathString = QString());
bool handleUsername(const QString& lookupString);
bool handleDomainID(const QString& host);
diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp
index f34a93de96..df34a1fb59 100644
--- a/libraries/networking/src/DomainHandler.cpp
+++ b/libraries/networking/src/DomainHandler.cpp
@@ -15,6 +15,10 @@
#include
+#include
+
+#include
+
#include
#include
@@ -134,6 +138,18 @@ void DomainHandler::hardReset() {
_pendingPath.clear();
}
+bool DomainHandler::getInterstitialModeEnabled() const {
+ return _interstitialModeSettingLock.resultWithReadLock([&] {
+ return _enableInterstitialMode.get();
+ });
+}
+
+void DomainHandler::setInterstitialModeEnabled(bool enableInterstitialMode) {
+ _interstitialModeSettingLock.withWriteLock([&] {
+ _enableInterstitialMode.set(enableInterstitialMode);
+ });
+}
+
void DomainHandler::setErrorDomainURL(const QUrl& url) {
_errorDomainURL = url;
return;
@@ -340,11 +356,15 @@ void DomainHandler::loadedErrorDomain(std::map namedPaths) {
DependencyManager::get()->goToViewpointForPath(viewpoint, QString());
}
-void DomainHandler::setRedirectErrorState(QUrl errorUrl, int reasonCode) {
- _errorDomainURL = errorUrl;
+void DomainHandler::setRedirectErrorState(QUrl errorUrl, QString reasonMessage, int reasonCode, const QString& extraInfo) {
_lastDomainConnectionError = reasonCode;
- _isInErrorState = true;
- emit redirectToErrorDomainURL(_errorDomainURL);
+ if (getInterstitialModeEnabled()) {
+ _errorDomainURL = errorUrl;
+ _isInErrorState = true;
+ emit redirectToErrorDomainURL(_errorDomainURL);
+ } else {
+ emit domainConnectionRefused(reasonMessage, reasonCode, extraInfo);
+ }
}
void DomainHandler::requestDomainSettings() {
@@ -485,13 +505,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer
#include
+#include
+#include
+
#include "HifiSockAddr.h"
#include "NetworkPeer.h"
#include "NLPacket.h"
@@ -83,6 +86,8 @@ public:
bool isConnected() const { return _isConnected; }
void setIsConnected(bool isConnected);
bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; }
+ bool getInterstitialModeEnabled() const;
+ void setInterstitialModeEnabled(bool enableInterstitialMode);
void connectedToServerless(std::map namedPaths);
@@ -171,7 +176,7 @@ public slots:
void processDomainServerConnectionDeniedPacket(QSharedPointer message);
// sets domain handler in error state.
- void setRedirectErrorState(QUrl errorUrl, int reasonCode);
+ void setRedirectErrorState(QUrl errorUrl, QString reasonMessage = "", int reason = -1, const QString& extraInfo = "");
bool isInErrorState() { return _isInErrorState; }
@@ -224,6 +229,8 @@ private:
QJsonObject _settingsObject;
QString _pendingPath;
QTimer _settingsTimer;
+ mutable ReadWriteLockable _interstitialModeSettingLock;
+ Setting::Handle _enableInterstitialMode{ "enableInterstitialMode", false };
QSet _domainConnectionRefusals;
bool _hasCheckedForAccessToken { false };
diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp
index 031baece5f..26460cbdd7 100644
--- a/libraries/pointers/src/Pointer.cpp
+++ b/libraries/pointers/src/Pointer.cpp
@@ -68,7 +68,8 @@ void Pointer::update(unsigned int pointerID) {
// This only needs to be a read lock because update won't change any of the properties that can be modified from scripts
withReadLock([&] {
auto pickResult = getPrevPickResult();
- auto visualPickResult = getVisualPickResult(pickResult);
+ // Pointer needs its own PickResult object so it doesn't modify the cached pick result
+ auto visualPickResult = getVisualPickResult(getPickResultCopy(pickResult));
updateVisuals(visualPickResult);
generatePointerEvents(pointerID, visualPickResult);
});
diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h
index 4264a60079..173163374f 100644
--- a/libraries/pointers/src/Pointer.h
+++ b/libraries/pointers/src/Pointer.h
@@ -91,6 +91,7 @@ protected:
virtual bool shouldHover(const PickResultPointer& pickResult) { return true; }
virtual bool shouldTrigger(const PickResultPointer& pickResult) { return true; }
+ virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const = 0;
virtual PickResultPointer getVisualPickResult(const PickResultPointer& pickResult) { return pickResult; };
static const float POINTER_MOVE_DELAY;
diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp
index 11326b1120..bcac31dd5a 100644
--- a/libraries/render-utils/src/HighlightEffect.cpp
+++ b/libraries/render-utils/src/HighlightEffect.cpp
@@ -37,6 +37,8 @@ namespace gr {
#define OUTLINE_STENCIL_MASK 1
+extern void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state);
+
HighlightRessources::HighlightRessources() {
}
@@ -180,6 +182,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
auto maskPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder);
auto maskSkinnedPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder.withSkinned());
+ auto maskSkinnedDQPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder.withSkinned().withDualQuatSkinned());
// Setup camera, projection and viewport for all items
batch.setViewportTransform(args->_viewport);
@@ -187,14 +190,17 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
batch.setProjectionJitter(jitter.x, jitter.y);
batch.setViewTransform(viewMat);
- std::vector skinnedShapeKeys{};
+ std::vector skinnedShapeKeys;
+ std::vector skinnedDQShapeKeys;
// Iterate through all inShapes and render the unskinned
args->_shapePipeline = maskPipeline;
batch.setPipeline(maskPipeline->pipeline);
for (const auto& items : inShapes) {
itemBounds.insert(itemBounds.end(), items.second.begin(), items.second.end());
- if (items.first.isSkinned()) {
+ if (items.first.isSkinned() && items.first.isDualQuatSkinned()) {
+ skinnedDQShapeKeys.push_back(items.first);
+ } else if (items.first.isSkinned()) {
skinnedShapeKeys.push_back(items.first);
} else {
renderItems(renderContext, items.second);
@@ -202,10 +208,21 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
}
// Reiterate to render the skinned
- args->_shapePipeline = maskSkinnedPipeline;
- batch.setPipeline(maskSkinnedPipeline->pipeline);
- for (const auto& key : skinnedShapeKeys) {
- renderItems(renderContext, inShapes.at(key));
+ if (skinnedShapeKeys.size() > 0) {
+ args->_shapePipeline = maskSkinnedPipeline;
+ batch.setPipeline(maskSkinnedPipeline->pipeline);
+ for (const auto& key : skinnedShapeKeys) {
+ renderItems(renderContext, inShapes.at(key));
+ }
+ }
+
+ // Reiterate to render the DQ skinned
+ if (skinnedDQShapeKeys.size() > 0) {
+ args->_shapePipeline = maskSkinnedDQPipeline;
+ batch.setPipeline(maskSkinnedDQPipeline->pipeline);
+ for (const auto& key : skinnedDQShapeKeys) {
+ renderItems(renderContext, inShapes.at(key));
+ }
}
args->_shapePipeline = nullptr;
@@ -488,7 +505,7 @@ void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, ren
state->setDepthTest(true, true, gpu::LESS_EQUAL);
state->setColorWriteMask(false, false, false, false);
- initMaskPipelines(*shapePlumber, state);
+ initZPassPipelines(*shapePlumber, state);
}
auto sharedParameters = std::make_shared();
@@ -548,16 +565,4 @@ const render::Varying DrawHighlightTask::addSelectItemJobs(JobModel& task, const
const auto selectedMetasAndOpaques = task.addJob("OpaqueSelection", selectMetaAndOpaqueInput);
const auto selectItemInput = SelectItems::Inputs(transparents, selectedMetasAndOpaques, selectionName).asVarying();
return task.addJob("TransparentSelection", selectItemInput);
-}
-
-void DrawHighlightTask::initMaskPipelines(render::ShapePlumber& shapePlumber, gpu::StatePointer state) {
- gpu::ShaderPointer modelProgram = gpu::Shader::createProgram(shader::render_utils::program::model_shadow);
- shapePlumber.addPipeline(
- ShapeKey::Filter::Builder().withoutSkinned(),
- modelProgram, state);
-
- gpu::ShaderPointer skinProgram = gpu::Shader::createProgram(shader::render_utils::program::skin_model_shadow);
- shapePlumber.addPipeline(
- ShapeKey::Filter::Builder().withSkinned(),
- skinProgram, state);
-}
+}
\ No newline at end of file
diff --git a/libraries/render-utils/src/HighlightEffect.h b/libraries/render-utils/src/HighlightEffect.h
index 64a97a549e..32668c1ab6 100644
--- a/libraries/render-utils/src/HighlightEffect.h
+++ b/libraries/render-utils/src/HighlightEffect.h
@@ -208,8 +208,6 @@ public:
void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs);
private:
-
- static void initMaskPipelines(render::ShapePlumber& plumber, gpu::StatePointer state);
static const render::Varying addSelectItemJobs(JobModel& task, const render::Varying& selectionName, const RenderFetchCullSortTask::BucketList& items);
};
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/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp
index bb22a1e753..012e7aa1f5 100644
--- a/libraries/shared/src/SharedUtil.cpp
+++ b/libraries/shared/src/SharedUtil.cpp
@@ -19,8 +19,8 @@
#include
#include
#include
-#include
#include
+#include
#include
@@ -127,82 +127,10 @@ void usecTimestampNowForceClockSkew(qint64 clockSkew) {
::usecTimestampNowAdjust = clockSkew;
}
-static std::atomic TIME_REFERENCE { 0 }; // in usec
-static std::once_flag usecTimestampNowIsInitialized;
-static QElapsedTimer timestampTimer;
-
quint64 usecTimestampNow(bool wantDebug) {
- std::call_once(usecTimestampNowIsInitialized, [&] {
- TIME_REFERENCE = QDateTime::currentMSecsSinceEpoch() * USECS_PER_MSEC; // ms to usec
- timestampTimer.start();
- });
-
- quint64 now;
- quint64 nsecsElapsed = timestampTimer.nsecsElapsed();
- quint64 usecsElapsed = nsecsElapsed / NSECS_PER_USEC; // nsec to usec
-
- // QElapsedTimer may not advance if the CPU has gone to sleep. In which case it
- // will begin to deviate from real time. We detect that here, and reset if necessary
- quint64 msecsCurrentTime = QDateTime::currentMSecsSinceEpoch();
- quint64 msecsEstimate = (TIME_REFERENCE + usecsElapsed) / USECS_PER_MSEC; // usecs to msecs
- int possibleSkew = msecsEstimate - msecsCurrentTime;
- const int TOLERANCE = 10 * MSECS_PER_SECOND; // up to 10 seconds of skew is tolerated
- if (abs(possibleSkew) > TOLERANCE) {
- // reset our TIME_REFERENCE and timer
- TIME_REFERENCE = QDateTime::currentMSecsSinceEpoch() * USECS_PER_MSEC; // ms to usec
- timestampTimer.restart();
- now = TIME_REFERENCE + ::usecTimestampNowAdjust;
-
- if (wantDebug) {
- qCDebug(shared) << "usecTimestampNow() - resetting QElapsedTimer. ";
- qCDebug(shared) << " msecsCurrentTime:" << msecsCurrentTime;
- qCDebug(shared) << " msecsEstimate:" << msecsEstimate;
- qCDebug(shared) << " possibleSkew:" << possibleSkew;
- qCDebug(shared) << " TOLERANCE:" << TOLERANCE;
-
- qCDebug(shared) << " nsecsElapsed:" << nsecsElapsed;
- qCDebug(shared) << " usecsElapsed:" << usecsElapsed;
-
- QDateTime currentLocalTime = QDateTime::currentDateTime();
-
- quint64 msecsNow = now / 1000; // usecs to msecs
- QDateTime nowAsString;
- nowAsString.setMSecsSinceEpoch(msecsNow);
-
- qCDebug(shared) << " now:" << now;
- qCDebug(shared) << " msecsNow:" << msecsNow;
-
- qCDebug(shared) << " nowAsString:" << nowAsString.toString("yyyy-MM-dd hh:mm:ss.zzz");
- qCDebug(shared) << " currentLocalTime:" << currentLocalTime.toString("yyyy-MM-dd hh:mm:ss.zzz");
- }
- } else {
- now = TIME_REFERENCE + usecsElapsed + ::usecTimestampNowAdjust;
- }
-
- if (wantDebug) {
- QDateTime currentLocalTime = QDateTime::currentDateTime();
-
- quint64 msecsNow = now / 1000; // usecs to msecs
- QDateTime nowAsString;
- nowAsString.setMSecsSinceEpoch(msecsNow);
-
- quint64 msecsTimeReference = TIME_REFERENCE / 1000; // usecs to msecs
- QDateTime timeReferenceAsString;
- timeReferenceAsString.setMSecsSinceEpoch(msecsTimeReference);
-
- qCDebug(shared) << "usecTimestampNow() - details... ";
- qCDebug(shared) << " TIME_REFERENCE:" << TIME_REFERENCE;
- qCDebug(shared) << " timeReferenceAsString:" << timeReferenceAsString.toString("yyyy-MM-dd hh:mm:ss.zzz");
- qCDebug(shared) << " usecTimestampNowAdjust:" << usecTimestampNowAdjust;
- qCDebug(shared) << " nsecsElapsed:" << nsecsElapsed;
- qCDebug(shared) << " usecsElapsed:" << usecsElapsed;
- qCDebug(shared) << " now:" << now;
- qCDebug(shared) << " msecsNow:" << msecsNow;
- qCDebug(shared) << " nowAsString:" << nowAsString.toString("yyyy-MM-dd hh:mm:ss.zzz");
- qCDebug(shared) << " currentLocalTime:" << currentLocalTime.toString("yyyy-MM-dd hh:mm:ss.zzz");
- }
-
- return now;
+ using namespace std::chrono;
+ static const auto unixEpoch = system_clock::from_time_t(0);
+ return duration_cast(system_clock::now() - unixEpoch).count() + usecTimestampNowAdjust;
}
float secTimestampNow() {
diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js
index 0e7461c5f1..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,7 +332,8 @@ 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 () {
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 993ea30c2e..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,15 +488,77 @@ 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);
+ }
+ if (!Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL)) {
+ Menu.addMenuItem({
+ menuName: DEVELOPER_MENU,
+ menuItemName: MARKETPLACE_ITEM_TESTER_LABEL,
+ isCheckable: false
+ });
+ }
+
+ Menu.menuItemEvent.connect(function (menuItem) {
+ if (menuItem === MARKETPLACE_ITEM_TESTER_LABEL) {
+ ui.open(MARKETPLACE_ITEM_TESTER_QML_SOURCE);
+ }
+ });
+}
+
+function uninstallMarketplaceItemTester() {
+ if (Menu.menuExists(DEVELOPER_MENU) &&
+ Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL)
+ ) {
+ Menu.removeMenuItem(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL);
+ }
+}
+
var BUTTON_NAME = "WALLET";
var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml";
var ui;
@@ -510,10 +569,18 @@ 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();
}
+
var isUpdateOverlaysWired = false;
function off() {
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
@@ -528,9 +595,11 @@ function off() {
}
removeOverlays();
}
+
function shutdown() {
GlobalServices.myUsernameChanged.disconnect(onUsernameChanged);
deleteSendMoneyParticleEffect();
+ uninstallMarketplaceItemTester();
off();
}
diff --git a/scripts/system/controllers/controllerModules/highlightNearbyEntities.js b/scripts/system/controllers/controllerModules/highlightNearbyEntities.js
index 3a33082f64..bc09ebee7a 100644
--- a/scripts/system/controllers/controllerModules/highlightNearbyEntities.js
+++ b/scripts/system/controllers/controllerModules/highlightNearbyEntities.js
@@ -37,7 +37,7 @@
this.highlightedEntities = [];
this.parameters = dispatcherUtils.makeDispatcherModuleParameters(
- 120,
+ 480,
this.hand === dispatcherUtils.RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js
index 2bdd89f141..d590545532 100644
--- a/scripts/system/controllers/controllerModules/inEditMode.js
+++ b/scripts/system/controllers/controllerModules/inEditMode.js
@@ -29,7 +29,7 @@ Script.include("/~/system/libraries/utils.js");
this.reticleMaxY;
this.parameters = makeDispatcherModuleParameters(
- 200,
+ 160,
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"],
[],
100,
diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js
index 02863cf935..7b78d5e1c4 100644
--- a/scripts/system/controllers/controllerModules/inVREditMode.js
+++ b/scripts/system/controllers/controllerModules/inVREditMode.js
@@ -21,7 +21,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
this.disableModules = false;
var NO_HAND_LASER = -1; // Invalid hand parameter so that default laser is not displayed.
this.parameters = makeDispatcherModuleParameters(
- 240, // Not too high otherwise the tablet laser doesn't work.
+ 200, // Not too high otherwise the tablet laser doesn't work.
this.hand === RIGHT_HAND
? ["rightHand", "rightHandEquip", "rightHandTrigger"]
: ["leftHand", "leftHandEquip", "leftHandTrigger"],
diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
index 27c1b458b8..a8de76aebd 100644
--- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
@@ -26,7 +26,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
this.hapticTargetID = null;
this.parameters = makeDispatcherModuleParameters(
- 140,
+ 500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
diff --git a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
index 366fcd3032..962ae89bb9 100644
--- a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
+++ b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
@@ -21,7 +21,7 @@
this.hyperlink = "";
this.parameters = makeDispatcherModuleParameters(
- 125,
+ 485,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
index f805dbf60e..cc88371441 100644
--- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
@@ -57,7 +57,7 @@ Script.include("/~/system/libraries/controllers.js");
this.cloneAllowed = true;
this.parameters = makeDispatcherModuleParameters(
- 140,
+ 500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js
index f1126dedc3..6a9cd9fbcd 100644
--- a/scripts/system/controllers/controllerModules/nearTrigger.js
+++ b/scripts/system/controllers/controllerModules/nearTrigger.js
@@ -29,7 +29,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
this.startSent = false;
this.parameters = makeDispatcherModuleParameters(
- 120,
+ 480,
this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "leftHand"],
[],
100);
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index 4e36355621..2412e2fa1c 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -121,7 +121,7 @@ Script.include("/~/system/libraries/controllers.js");
controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
var allowThisModule = !otherModuleRunning || isTriggerPressed;
- if (allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) {
+ if ((allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) && !this.grabModuleWantsNearbyOverlay(controllerData)) {
this.updateAllwaysOn();
if (isTriggerPressed) {
this.dominantHandOverride = true; // Override dominant hand.
diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js
index 13ad1f6b69..85cd499d20 100644
--- a/scripts/system/marketplaces/marketplaces.js
+++ b/scripts/system/marketplaces/marketplaces.js
@@ -20,13 +20,14 @@ var AppUi = Script.require('appUi');
Script.include("/~/system/libraries/gridTool.js");
Script.include("/~/system/libraries/connectionUtils.js");
-var METAVERSE_SERVER_URL = Account.metaverseServerURL;
-var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
-var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml";
+var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml";
+var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml";
var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml";
var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml";
-var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml";
+var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
+var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
+var METAVERSE_SERVER_URL = Account.metaverseServerURL;
var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav"));
// Event bridge messages.
@@ -137,7 +138,6 @@ function onUsernameChanged() {
}
}
-var userHasUpdates = false;
function sendCommerceSettings() {
ui.sendToHtml({
type: "marketplaces",
@@ -147,7 +147,7 @@ function sendCommerceSettings() {
userIsLoggedIn: Account.loggedIn,
walletNeedsSetup: Wallet.walletStatus === 1,
metaverseServerURL: Account.metaverseServerURL,
- messagesWaiting: userHasUpdates
+ messagesWaiting: shouldShowDot
}
});
}
@@ -756,7 +756,7 @@ function deleteSendAssetParticleEffect() {
}
sendAssetRecipient = null;
}
-
+
var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview");
var UI_FADE_TIMEOUT_MS = 150;
function maybeEnableHMDPreview() {
@@ -768,6 +768,13 @@ function maybeEnableHMDPreview() {
}, UI_FADE_TIMEOUT_MS);
}
+var resourceObjectsInTest = [];
+function signalNewResourceObjectInTest(resourceObject) {
+ ui.tablet.sendToQml({
+ method: "newResourceObjectInTest",
+ resourceObject: resourceObject });
+}
+
var onQmlMessageReceived = function onQmlMessageReceived(message) {
if (message.messageSrc === "HTML") {
return;
@@ -817,8 +824,20 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) {
break;
case 'checkout_rezClicked':
case 'purchases_rezClicked':
+ case 'tester_rezClicked':
rezEntity(message.itemHref, message.itemType);
break;
+ case 'tester_newResourceObject':
+ var resourceObject = message.resourceObject;
+ resourceObjectsInTest[resourceObject.id] = resourceObject;
+ signalNewResourceObjectInTest(resourceObject);
+ break;
+ case 'tester_updateResourceObjectAssetType':
+ resourceObjectsInTest[message.objectId].assetType = message.assetType;
+ break;
+ case 'tester_deleteResourceObject':
+ delete resourceObjectsInTest[message.objectId];
+ break;
case 'header_marketplaceImageClicked':
case 'purchases_backClicked':
ui.open(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL);
@@ -841,10 +860,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) {
openLoginWindow();
break;
case 'disableHmdPreview':
- if (!savedDisablePreviewOption) {
- savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview");
- }
-
if (!savedDisablePreviewOption) {
DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN");
Menu.setIsOptionChecked("Disable Preview", true);
@@ -908,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 = [];
@@ -962,34 +976,65 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) {
}
};
+function pushResourceObjectsInTest() {
+ var maxObjectId = -1;
+ for (var objectId in resourceObjectsInTest) {
+ signalNewResourceObjectInTest(resourceObjectsInTest[objectId]);
+ maxObjectId = (maxObjectId < objectId) ? parseInt(objectId) : maxObjectId;
+ }
+ // N.B. Thinking about removing the following sendToQml? Be sure
+ // that the marketplace item tester QML has heard from us, at least
+ // so that it can indicate to the user that all of the resoruce
+ // objects in test have been transmitted to it.
+ ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxObjectId + 1 });
+}
+
// Function Name: onTabletScreenChanged()
//
// Description:
// -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string
// value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML.
-var onMarketplaceScreen = false;
-var onWalletScreen = false;
+
var onCommerceScreen = false;
var onInspectionCertificateScreen = false;
+var onMarketplaceItemTesterScreen = false;
+var onMarketplaceScreen = false;
+var onWalletScreen = false;
var onTabletScreenChanged = function onTabletScreenChanged(type, url) {
ui.setCurrentVisibleScreenMetadata(type, url);
onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1;
onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1;
var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1;
- var onCommerceScreenNow = type === "QML" &&
- (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH ||
- onInspectionCertificateScreen);
+ var onCommerceScreenNow = type === "QML" && (
+ url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 ||
+ url === MARKETPLACE_PURCHASES_QML_PATH ||
+ url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1);
+ var onMarketplaceItemTesterScreenNow = (
+ url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 ||
+ url === MARKETPLACE_ITEM_TESTER_QML_PATH);
- // exiting wallet or commerce screen
- if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) {
+ if ((!onWalletScreenNow && onWalletScreen) ||
+ (!onCommerceScreenNow && onCommerceScreen) ||
+ (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen)
+ ) {
+ // exiting wallet, commerce, or marketplace item tester screen
maybeEnableHMDPreview();
}
onCommerceScreen = onCommerceScreenNow;
onWalletScreen = onWalletScreenNow;
- wireQmlEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen);
+ onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow;
+
+ wireQmlEventBridge(
+ onMarketplaceScreen ||
+ onCommerceScreen ||
+ onWalletScreen ||
+ onMarketplaceItemTesterScreen);
if (url === MARKETPLACE_PURCHASES_QML_PATH) {
+ // FIXME? Is there a race condition here in which the event
+ // bridge may not be up yet? If so, Script.setTimeout(..., 750)
+ // may help avoid the condition.
ui.tablet.sendToQml({
method: 'updatePurchases',
referrerURL: referrerURL,
@@ -1026,13 +1071,53 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) {
});
off();
}
+
+ if (onMarketplaceItemTesterScreen) {
+ // Why setTimeout? The QML event bridge, wired above, requires a
+ // variable amount of time to come up, in practice less than
+ // 750ms.
+ Script.setTimeout(pushResourceObjectsInTest, 750);
+ }
+
console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type +
"\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.
@@ -1044,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/CMakeLists.txt b/server-console/CMakeLists.txt
index df51409151..8510441b43 100644
--- a/server-console/CMakeLists.txt
+++ b/server-console/CMakeLists.txt
@@ -36,6 +36,7 @@ if (APPLE)
PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}"
DESTINATION ${CONSOLE_INSTALL_DIR}
COMPONENT ${SERVER_COMPONENT}
+ COMPONENT ${CLIENT_COMPONENT}
)
install(
PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}"
@@ -49,6 +50,7 @@ elseif (WIN32)
DIRECTORY "${CONSOLE_DESTINATION}/"
DESTINATION ${CONSOLE_INSTALL_DIR}
COMPONENT ${SERVER_COMPONENT}
+ COMPONENT ${CLIENT_COMPONENT}
)
install(
DIRECTORY "${CONSOLE_DESTINATION}/"
diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js
index ab4bd44486..2953075523 100644
--- a/server-console/src/modules/hf-notifications.js
+++ b/server-console/src/modules/hf-notifications.js
@@ -16,6 +16,7 @@ const WALLET_NOTIFICATION_POLL_TIME_MS = 600 * 1000;
const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000;
const OSX_CLICK_DELAY_TIMEOUT = 500;
+
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';