diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8b3f09d1f7..88f4b94883 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1083,9 +1083,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { // Setup the domain object to send to the data server QJsonObject domainObject; - // add the version + // add the versions static const QString VERSION_KEY = "version"; domainObject[VERSION_KEY] = BuildInfo::VERSION; + static const QString PROTOCOL_KEY = "protocol"; + domainObject[PROTOCOL_KEY] = protocolVersionsSignatureBase64(); // add networking if (!networkAddress.isEmpty()) { @@ -1119,7 +1121,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), + QString path = DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())); +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata sent to" << path; + qDebug() << "Domain metadata update:" << domainUpdateJSON; +#endif + DependencyManager::get()->sendRequest(path, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, JSONCallbackParameters(nullptr, QString(), this, "handleMetaverseHeartbeatError"), diff --git a/interface/resources/images/default-domain.gif b/interface/resources/images/default-domain.gif new file mode 100644 index 0000000000..1418a54cdd Binary files /dev/null and b/interface/resources/images/default-domain.gif differ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index dc060b70e1..792410c59d 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -13,6 +13,7 @@ import QtQuick 2.4 import "controls" import "styles" import "windows" +import "hifi" Window { id: root @@ -44,11 +45,50 @@ Window { anchors.centerIn = parent; } + function goCard(card) { + addressLine.text = card.userStory.name; + toggleOrGo(true); + } + property var allDomains: []; + property var suggestionChoices: []; + property var domainsBaseUrl: null; + property int cardWidth: 200; + property int cardHeight: 152; + AddressBarDialog { id: addressBarDialog implicitWidth: backgroundImage.width implicitHeight: backgroundImage.height + Row { + width: backgroundImage.width; + anchors { + bottom: backgroundImage.top; + bottomMargin: 2 * hifi.layout.spacing; + right: backgroundImage.right; + rightMargin: -104; // FIXME + } + spacing: hifi.layout.spacing; + Card { + id: s0; + width: cardWidth; + height: cardHeight; + goFunction: goCard + } + Card { + id: s1; + width: cardWidth; + height: cardHeight; + goFunction: goCard + } + Card { + id: s2; + width: cardWidth; + height: cardHeight; + goFunction: goCard + } + } + Image { id: backgroundImage source: "../images/address-bar.svg" @@ -130,22 +170,178 @@ Window { } font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 helperText: "Go to: place, @user, /path, network address" + onTextChanged: filterChoicesByText() } } } + function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. + // TODO: make available to other .qml. + var request = new XMLHttpRequest(); + // QT bug: apparently doesn't handle onload. Workaround using readyState. + request.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (request.readyState >= READY_STATE_DONE) { + var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, + response = !error && request.responseText, + contentType = !error && request.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + cb(error, response); + } + }; + request.open("GET", url, true); + request.send(); + } + // call iterator(element, icb) once for each element of array, and then cb(error) when icb(error) has been called by each iterator. + // short-circuits if error. Note that iterator MUST be an asynchronous function. (Use setTimeout if necessary.) + function asyncEach(array, iterator, cb) { + var count = array.length; + function icb(error) { + if (!--count || error) { + count = -1; // don't cb multiple times (e.g., if error) + cb(error); + } + } + if (!count) { + return cb(); + } + array.forEach(function (element) { + iterator(element, icb); + }); + } + + function identity(x) { + return x; + } + + function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) + // This requests data for all the names at once, and just uses the first one to come back. + // We might change this to check one at a time, which would be less requests and more latency. + asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(identity), function (name, icb) { + var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; + getRequest(url, function (error, json) { + var previews = !error && json.data.place.previews; + if (previews) { + if (!domainInfo.thumbnail) { // just grab the first one + domainInfo.thumbnail = previews.thumbnail; + } + if (!domainInfo.lobby) { + domainInfo.lobby = previews.lobby; + } + } + icb(error); + }); + }, cb); + } + + function getDomains(options, cb) { // cb(error, arrayOfData) + if (!options.page) { + options.page = 1; + } + if (!domainsBaseUrl) { + var domainsOptions = [ + 'open', // published hours handle now + 'active', // has at least one person connected. FIXME: really want any place that is verified accessible. + // FIXME: really want places I'm allowed in, not just open ones. + 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. + // FIXME add maturity + 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), + 'sort_by=users', + 'sort_order=desc', + ]; + domainsBaseUrl = "https://metaverse.highfidelity.com/api/v1/domains/all?" + domainsOptions.join('&'); + } + var url = domainsBaseUrl + "&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; + getRequest(url, function (error, json) { + if (!error && (json.status !== 'success')) { + error = new Error("Bad response: " + JSON.stringify(json)); + } + if (error) { + error.message += ' for ' + url; + return cb(error); + } + var domains = json.data.domains; + if (json.current_page < json.total_pages) { + options.page++; + return getDomains(options, function (error, others) { + cb(error, domains.concat(others)); + }); + } + cb(null, domains); + }); + } + + function filterChoicesByText() { + function fill1(target, data) { + if (!data) { + target.visible = false; + return; + } + console.log('suggestion:', JSON.stringify(data)); + target.userStory = data; + target.image.source = data.lobby || target.defaultPicture; + target.placeText = data.name; + target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); + target.visible = true; + } + var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); + var filtered = !words.length ? suggestionChoices : allDomains.filter(function (domain) { + var text = domain.names.concat(domain.tags).join(' '); + if (domain.description) { + text += domain.description; + } + text = text.toUpperCase(); + return words.every(function (word) { + return text.indexOf(word) >= 0; + }); + }); + fill1(s0, filtered[0]); + fill1(s1, filtered[1]); + fill1(s2, filtered[2]); + } + + function fillDestinations() { + allDomains = suggestionChoices = []; + getDomains({minUsers: 0, maxUsers: 20}, function (error, domains) { + if (error) { + console.log('domain query failed:', error); + return filterChoicesByText(); + } + var here = AddressManager.hostname; // don't show where we are now. + allDomains = domains.filter(function (domain) { return domain.name !== here; }); + // Whittle down suggestions to those that have at least one user, and try to get pictures. + suggestionChoices = allDomains.filter(function (domain) { return domain.online_users; }); + asyncEach(domains, addPictureToDomain, function (error) { + if (error) { + console.log('place picture query failed:', error); + } + // Whittle down more by requiring a picture. + suggestionChoices = suggestionChoices.filter(function (domain) { return domain.lobby; }); + filterChoicesByText(); + }); + }); + } + onVisibleChanged: { if (visible) { addressLine.forceActiveFocus() + fillDestinations(); } else { addressLine.text = "" } } - function toggleOrGo() { + function toggleOrGo(fromSuggestions) { if (addressLine.text !== "") { - addressBarDialog.loadAddress(addressLine.text) + addressBarDialog.loadAddress(addressLine.text, fromSuggestions) } root.shown = false; } diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml new file mode 100644 index 0000000000..7758c5800a --- /dev/null +++ b/interface/resources/qml/hifi/Card.qml @@ -0,0 +1,96 @@ +// +// Card.qml +// qml/hifi +// +// Displays a clickable card representing a user story or destination. +// +// Created by Howard Stearns on 7/13/2016 +// Copyright 2016 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 Hifi 1.0 +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import "../styles-uit" + +Rectangle { + property var goFunction: null; + property var userStory: null; + property alias image: lobby; + property alias placeText: place.text; + property alias usersText: users.text; + property int textPadding: 20; + property int textSize: 24; + property string defaultPicture: "../../images/default-domain.gif"; + HifiConstants { id: hifi } + Image { + id: lobby; + width: parent.width; + height: parent.height; + source: defaultPicture; + fillMode: Image.PreserveAspectCrop; + // source gets filled in later + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + onStatusChanged: { + if (status == Image.Error) { + console.log("source: " + source + ": failed to load " + JSON.stringify(userStory)); + source = defaultPicture; + } + } + } + property int dropHorizontalOffset: 0; + property int dropVerticalOffset: 1; + property int dropRadius: 2; + property int dropSamples: 9; + property int dropSpread: 0; + DropShadow { + source: place; + anchors.fill: place; + horizontalOffset: dropHorizontalOffset; + verticalOffset: dropVerticalOffset; + radius: dropRadius; + samples: dropSamples; + color: hifi.colors.black; + spread: dropSpread; + } + DropShadow { + source: users; + anchors.fill: users; + horizontalOffset: dropHorizontalOffset; + verticalOffset: dropVerticalOffset; + radius: dropRadius; + samples: dropSamples; + color: hifi.colors.black; + spread: dropSpread; + } + RalewaySemiBold { + id: place; + color: hifi.colors.white; + size: textSize; + anchors { + top: parent.top; + left: parent.left; + margins: textPadding; + } + } + RalewayRegular { + id: users; + size: textSize; + color: hifi.colors.white; + anchors { + bottom: parent.bottom; + right: parent.right; + margins: textPadding; + } + } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: goFunction(parent); + hoverEnabled: true; + } +} diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 6fb437e312..a4ef8a913f 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -40,10 +40,10 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); } -void AddressBarDialog::loadAddress(const QString& address) { +void AddressBarDialog::loadAddress(const QString& address, bool fromSuggestions) { qDebug() << "Called LoadAddress with address " << address; if (!address.isEmpty()) { - DependencyManager::get()->handleLookupString(address); + DependencyManager::get()->handleLookupString(address, fromSuggestions); } } diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index bbce52c67c..6c7620164b 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -34,7 +34,7 @@ protected: void displayAddressOfflineMessage(); void displayAddressNotFoundMessage(); - Q_INVOKABLE void loadAddress(const QString& address); + Q_INVOKABLE void loadAddress(const QString& address, bool fromSuggestions = false); Q_INVOKABLE void loadHome(); Q_INVOKABLE void loadBack(); Q_INVOKABLE void loadForward(); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 7ed3888be0..ae6aad3c4f 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -24,6 +24,7 @@ #include "NodeList.h" #include "NetworkLogging.h" #include "UserActivityLogger.h" +#include "udt/PacketHeaders.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -37,6 +38,10 @@ AddressManager::AddressManager() : } +QString AddressManager::protocolVersion() { + return protocolVersionsSignatureBase64(); +} + bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } @@ -221,7 +226,7 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { return false; } -void AddressManager::handleLookupString(const QString& lookupString) { +void AddressManager::handleLookupString(const QString& lookupString, bool fromSuggestions) { if (!lookupString.isEmpty()) { // make this a valid hifi URL and handle it off to handleUrl QString sanitizedString = lookupString.trimmed(); @@ -236,7 +241,7 @@ void AddressManager::handleLookupString(const QString& lookupString) { lookupURL = QUrl(lookupString); } - handleUrl(lookupURL); + handleUrl(lookupURL, fromSuggestions ? Suggestions : UserInput); } } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index a3aaee3ba2..2e9f177137 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -39,6 +39,7 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString hostname READ getHost) Q_PROPERTY(QString pathname READ currentPath) public: + Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; using OrientationGetter = std::function; @@ -49,7 +50,8 @@ public: StartupFromSettings, DomainPathResponse, Internal, - AttemptedRefresh + AttemptedRefresh, + Suggestions }; bool isConnected(); @@ -77,7 +79,7 @@ public: std::function localSandboxNotRunningDoThat); public slots: - void handleLookupString(const QString& lookupString); + void handleLookupString(const QString& lookupString, bool fromSuggestions = false); // 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) diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index eba4d31167..75e15db2a4 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -178,6 +178,9 @@ void UserActivityLogger::wentTo(AddressManager::LookupTrigger lookupTrigger, QSt case AddressManager::StartupFromSettings: trigger = "StartupFromSettings"; break; + case AddressManager::Suggestions: + trigger = "Suggesions"; + break; default: return; } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index eb8739dd49..ce1f25d45d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -106,8 +106,9 @@ void sendWrongProtocolVersionsSignature(bool sendWrongVersion) { } #endif -QByteArray protocolVersionsSignature() { - static QByteArray protocolVersionSignature; +static QByteArray protocolVersionSignature; +static QString protocolVersionSignatureBase64; +static void ensureProtocolVersionsSignature() { static std::once_flag once; std::call_once(once, [&] { QByteArray buffer; @@ -121,8 +122,11 @@ QByteArray protocolVersionsSignature() { QCryptographicHash hash(QCryptographicHash::Md5); hash.addData(buffer); protocolVersionSignature = hash.result(); + protocolVersionSignatureBase64 = protocolVersionSignature.toBase64(); }); - +} +QByteArray protocolVersionsSignature() { + ensureProtocolVersionsSignature(); #if (PR_BUILD || DEV_BUILD) if (sendWrongProtocolVersion) { return QByteArray("INCORRECTVERSION"); // only for debugging version checking @@ -131,3 +135,7 @@ QByteArray protocolVersionsSignature() { return protocolVersionSignature; } +QString protocolVersionsSignatureBase64() { + ensureProtocolVersionsSignature(); + return protocolVersionSignatureBase64; +} diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e723ea38eb..7281e24fa9 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -113,6 +113,7 @@ extern const QSet NON_SOURCED_PACKETS; PacketVersion versionForPacketType(PacketType packetType); QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols +QString protocolVersionsSignatureBase64(); #if (PR_BUILD || DEV_BUILD) void sendWrongProtocolVersionsSignature(bool sendWrongVersion); /// for debugging version negotiation