diff --git a/android/app/build.gradle b/android/app/build.gradle
index 46de9642d9..699008092c 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -22,7 +22,7 @@ android {
'-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED,
'-DRELEASE_NUMBER=' + RELEASE_NUMBER,
'-DRELEASE_TYPE=' + RELEASE_TYPE,
- '-DBUILD_BRANCH=' + BUILD_BRANCH,
+ '-DSTABLE_BUILD=' + STABLE_BUILD,
'-DDISABLE_QML=OFF',
'-DDISABLE_KTX_CACHE=OFF'
}
@@ -128,4 +128,3 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
}
-
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
index e05b25f3c3..b98849d051 100644
--- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
+++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
@@ -64,7 +64,11 @@ public class HomeFragment extends Fragment {
mDomainsView.setLayoutManager(gridLayoutMgr);
mDomainAdapter = new DomainAdapter(getContext(), HifiUtils.getInstance().protocolVersionSignature(), nativeGetLastLocation());
mDomainAdapter.setClickListener((view, position, domain) -> {
- new Handler(getActivity().getMainLooper()).postDelayed(() -> mListener.onSelectedDomain(domain.url), 400); // a delay so the ripple effect can be seen
+ new Handler(getActivity().getMainLooper()).postDelayed(() -> {
+ if (mListener != null) {
+ mListener.onSelectedDomain(domain.url);
+ }
+ }, 400); // a delay so the ripple effect can be seen
});
mDomainAdapter.setListener(new DomainAdapter.AdapterListener() {
@Override
@@ -116,7 +120,9 @@ public class HomeFragment extends Fragment {
if (!urlString.trim().isEmpty()) {
urlString = HifiUtils.getInstance().sanitizeHifiUrl(urlString);
}
- mListener.onSelectedDomain(urlString);
+ if (mListener != null) {
+ mListener.onSelectedDomain(urlString);
+ }
return true;
}
return false;
diff --git a/android/build.gradle b/android/build.gradle
index f09dc97850..f1fe4ffc7f 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,7 +37,7 @@ task clean(type: Delete) {
ext {
RELEASE_NUMBER = project.hasProperty('RELEASE_NUMBER') ? project.getProperty('RELEASE_NUMBER') : '0'
RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV'
- BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : ''
+ STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0'
EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : ''
QT5_DEPS = [
'Qt5Concurrent',
@@ -542,7 +542,7 @@ task cleanDependencies(type: Delete) {
-// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution.
+// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution.
// See the comment on the qtBundle task above
/*
// FIXME derive the path from the gradle environment
diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake
index 36e5a065df..029c829022 100644
--- a/cmake/macros/SetPackagingParameters.cmake
+++ b/cmake/macros/SetPackagingParameters.cmake
@@ -17,14 +17,12 @@ macro(SET_PACKAGING_PARAMETERS)
set(DEV_BUILD 0)
set(BUILD_GLOBAL_SERVICES "DEVELOPMENT")
set(USE_STABLE_GLOBAL_SERVICES 0)
+ set(BUILD_NUMBER 0)
set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV")
set_from_env(RELEASE_NUMBER RELEASE_NUMBER "")
- set_from_env(BUILD_BRANCH BRANCH "")
- string(TOLOWER "${BUILD_BRANCH}" BUILD_BRANCH)
+ set_from_env(STABLE_BUILD STABLE_BUILD 0)
- message(STATUS "The BUILD_BRANCH variable is: ${BUILD_BRANCH}")
- message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}")
message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}")
# setup component categories for installer
@@ -46,17 +44,17 @@ macro(SET_PACKAGING_PARAMETERS)
# if the build is a PRODUCTION_BUILD from the "stable" branch
# then use the STABLE gobal services
- if (BUILD_BRANCH STREQUAL "stable")
- message(STATUS "The RELEASE_TYPE is PRODUCTION and the BUILD_BRANCH is stable...")
+ if (STABLE_BUILD)
+ message(STATUS "The RELEASE_TYPE is PRODUCTION and STABLE_BUILD is 1")
set(BUILD_GLOBAL_SERVICES "STABLE")
set(USE_STABLE_GLOBAL_SERVICES 1)
- endif()
+ endif ()
elseif (RELEASE_TYPE STREQUAL "PR")
set(DEPLOY_PACKAGE TRUE)
set(PR_BUILD 1)
set(BUILD_VERSION "PR${RELEASE_NUMBER}")
- set(BUILD_ORGANIZATION "High Fidelity - ${BUILD_VERSION}")
+ set(BUILD_ORGANIZATION "High Fidelity - PR${RELEASE_NUMBER}")
set(INTERFACE_BUNDLE_NAME "Interface")
set(INTERFACE_ICON_PREFIX "interface-beta")
@@ -75,6 +73,54 @@ macro(SET_PACKAGING_PARAMETERS)
string(TIMESTAMP BUILD_TIME "%d/%m/%Y")
+ # if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and
+ # DEV_BUILD and PR_BUILD must be 0
+ if (STABLE_BUILD)
+ if ((NOT PRODUCTION_BUILD) OR PR_BUILD OR DEV_BUILD)
+ message(FATAL_ERROR "Cannot produce STABLE_BUILD without PRODUCTION_BUILD")
+ endif ()
+ endif ()
+
+ if ((PRODUCTION_BUILD OR PR_BUILD) AND NOT STABLE_BUILD)
+ # append the abbreviated commit SHA to the build version
+ # since this is a PR build or master/nightly builds
+
+ # for PR_BUILDS, we need to grab the abbreviated SHA
+ # for the second parent of HEAD (not HEAD) since that is the
+ # SHA of the commit merged to master for the build
+ if (PR_BUILD)
+ set(_GIT_LOG_FORMAT "%p")
+ else ()
+ set(_GIT_LOG_FORMAT "%h")
+ endif ()
+
+ execute_process(
+ COMMAND git log -1 --format=${_GIT_LOG_FORMAT}
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ OUTPUT_VARIABLE _GIT_LOG_OUTPUT
+ ERROR_VARIABLE _GIT_LOG_ERROR
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+
+ if (PR_BUILD)
+ separate_arguments(_COMMIT_PARENTS UNIX_COMMAND ${_GIT_LOG_OUTPUT})
+ list(GET _COMMIT_PARENTS 1 GIT_COMMIT_HASH)
+ else ()
+ set(GIT_COMMIT_HASH ${_GIT_LOG_OUTPUT})
+ endif ()
+
+ if (_GIT_LOG_ERROR OR NOT GIT_COMMIT_HASH)
+ message(FATAL_ERROR "Could not retreive abbreviated SHA for PR or production master build")
+ endif ()
+
+ set(BUILD_VERSION_NO_SHA ${BUILD_VERSION})
+ set(BUILD_VERSION "${BUILD_VERSION}-${GIT_COMMIT_HASH}")
+
+ # pass along a release number without the SHA in case somebody
+ # wants to compare master or PR builds as integers
+ set(BUILD_NUMBER ${RELEASE_NUMBER})
+ endif ()
+
if (DEPLOY_PACKAGE)
# for deployed packages always grab the serverless content
set(DOWNLOAD_SERVERLESS_CONTENT ON)
@@ -127,8 +173,8 @@ macro(SET_PACKAGING_PARAMETERS)
set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface")
set(CONSOLE_SHORTCUT_NAME "Sandbox")
else ()
- set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION}")
- set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION}")
+ set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION_NO_SHA}")
+ set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION_NO_SHA}")
endif ()
set(INTERFACE_HF_SHORTCUT_NAME "${INTERFACE_SHORTCUT_NAME}")
diff --git a/cmake/templates/BuildInfo.h.in b/cmake/templates/BuildInfo.h.in
index 904d17293b..9fc9d9be81 100644
--- a/cmake/templates/BuildInfo.h.in
+++ b/cmake/templates/BuildInfo.h.in
@@ -24,8 +24,26 @@ namespace BuildInfo {
const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@";
const QString ORGANIZATION_DOMAIN = "highfidelity.io";
const QString VERSION = "@BUILD_VERSION@";
- const QString BUILD_BRANCH = "@BUILD_BRANCH@";
+ const QString BUILD_NUMBER = "@BUILD_NUMBER@";
const QString BUILD_GLOBAL_SERVICES = "@BUILD_GLOBAL_SERVICES@";
const QString BUILD_TIME = "@BUILD_TIME@";
-}
+ enum BuildType {
+ Dev,
+ PR,
+ Master,
+ Stable
+ };
+
+#if defined(PR_BUILD)
+ const BuildType BUILD_TYPE = PR;
+ const QString BUILD_TYPE_STRING = "pr";
+#elif defined(PRODUCTION_BUILD)
+ const BuildType BUILD_TYPE = @STABLE_BUILD@ ? Stable : Master;
+ const QString BUILD_TYPE_STRING = @STABLE_BUILD@ ? "stable" : "master";
+#else
+ const BuildType BUILD_TYPE = Dev;
+ const QString BUILD_TYPE_STRING = "dev";
+#endif
+
+}
diff --git a/cmake/templates/console-build-info.json.in b/cmake/templates/console-build-info.json.in
index c1ef010e08..6b4ee99292 100644
--- a/cmake/templates/console-build-info.json.in
+++ b/cmake/templates/console-build-info.json.in
@@ -1,4 +1,5 @@
{
"releaseType": "@RELEASE_TYPE@",
- "buildIdentifier": "@BUILD_VERSION@"
+ "buildIdentifier": "@BUILD_VERSION@",
+ "organization": "@BUILD_ORGANIZATION@"
}
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index 42a74aa2fc..e69e241182 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -176,7 +176,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
qDebug() << "[VERSION] Build sequence:" << qPrintable(applicationVersion());
qDebug() << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION;
qDebug() << "[VERSION] VERSION:" << BuildInfo::VERSION;
- qDebug() << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH;
+ qDebug() << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING;
qDebug() << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES;
qDebug() << "[VERSION] We will be using this name to find ICE servers:" << _iceServerAddr;
@@ -1122,7 +1122,7 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) {
}
void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) {
- const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID +
+ const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID +
NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + 4;
// setup the extended header for the domain list packets
@@ -2684,7 +2684,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : "";
QString hexHeaderPassword = headerPassword.isEmpty() ?
"" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
-
+
if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) {
return true;
}
diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml
index 6743d08275..917068ac01 100644
--- a/interface/resources/qml/controls-uit/TextField.qml
+++ b/interface/resources/qml/controls-uit/TextField.qml
@@ -165,11 +165,11 @@ TextField {
anchors.left: parent.left
Binding on anchors.right {
- when: parent.right
- value: parent.right
+ when: textField.right
+ value: textField.right
}
Binding on wrapMode {
- when: parent.right
+ when: textField.right
value: Text.WordWrap
}
diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml
index 3f3a47a297..d9e93c2fa7 100644
--- a/interface/resources/qml/hifi/Feed.qml
+++ b/interface/resources/qml/hifi/Feed.qml
@@ -16,10 +16,11 @@ import QtQuick 2.5
import QtGraphicalEffects 1.0
import "toolbars"
import "../styles-uit"
+import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
Column {
id: root;
- visible: false;
+ visible: !!suggestions.count;
property int cardWidth: 212;
property int cardHeight: 152;
@@ -32,21 +33,37 @@ Column {
property int stackedCardShadowHeight: 4;
property int labelSize: 20;
- property string metaverseServerUrl: '';
property string protocol: '';
property string actions: 'snapshot';
// sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick.
property string labelText: actions;
property string filter: '';
- onFilterChanged: filterChoicesByText();
property var goFunction: null;
- property var rpc: null;
+ property var http: null;
HifiConstants { id: hifi }
- ListModel { id: suggestions; }
+ Component.onCompleted: suggestions.getFirstPage();
+ HifiModels.PSFListModel {
+ id: suggestions;
+ http: root.http;
+ property var options: [
+ 'include_actions=' + actions,
+ 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
+ 'require_online=true',
+ 'protocol=' + encodeURIComponent(Window.protocolSignature())
+ ];
+ endpoint: '/api/v1/user_stories?' + options.join('&');
+ itemsPerPage: 3;
+ processPage: function (data) {
+ return data.user_stories.map(makeModelData);
+ };
+ listModelName: actions;
+ listView: scroll;
+ searchFilter: filter;
+ }
function resolveUrl(url) {
- return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url;
+ return (url.indexOf('/') === 0) ? (Account.metaverseServerURL + url) : url;
}
function makeModelData(data) { // create a new obj from data
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
@@ -55,16 +72,11 @@ Column {
tags = data.tags || [data.action, data.username],
description = data.description || "",
thumbnail_url = data.thumbnail_url || "";
- if (actions === 'concurrency,snapshot') {
- // A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements.
- data.details.connections = 4;
- data.action = 'announcement';
- }
return {
place_name: name,
username: data.username || "",
path: data.path || "",
- created_at: data.created_at || "",
+ created_at: data.created_at || data.updated_at || "", // FIXME why aren't we getting created_at?
action: data.action || "",
thumbnail_url: resolveUrl(thumbnail_url),
image_url: resolveUrl(data.details && data.details.image_url),
@@ -74,125 +86,9 @@ Column {
tags: tags,
description: description,
online_users: data.details.connections || data.details.concurrency || 0,
- drillDownToPlace: false,
-
- searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
- }
- }
- property var allStories: [];
- property var placeMap: ({}); // Used for making stacks.
- property int requestId: 0;
- function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey
- if (!error && (data.status === 'success')) {
- return;
- }
- if (!error) { // Create a message from the data
- error = data.status + ': ' + data.error;
- }
- if (typeof(error) === 'string') { // Make a proper Error object
- error = new Error(error);
- }
- error.message += ' in ' + url; // Include the url.
- cb(error);
- return true;
- }
- function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model
- // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness.
- var options = [
- 'now=' + new Date().toISOString(),
- 'include_actions=' + actions,
- 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
- 'require_online=true',
- 'protocol=' + protocol,
- 'page=' + pageNumber
- ];
- var url = metaverseBase + 'user_stories?' + options.join('&');
- var thisRequestId = ++requestId;
- rpc('request', url, function (error, data) {
- if (thisRequestId !== requestId) {
- error = 'stale';
- }
- if (handleError(url, error, data, cb)) {
- return; // abandon stale requests
- }
- allStories = allStories.concat(data.user_stories.map(makeModelData));
- if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
- if ((pageNumber === 1) && cb1) {
- cb1();
- }
- return getUserStoryPage(pageNumber + 1, cb);
- }
- cb();
- });
- }
- function fillDestinations() { // Public
- console.debug('Feed::fillDestinations()')
-
- function report(label, error) {
- console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count);
- }
- var filter = makeFilteredStoryProcessor(), counter = 0;
- allStories = [];
- suggestions.clear();
- placeMap = {};
- getUserStoryPage(1, function (error) {
- allStories.slice(counter).forEach(filter);
- report('user stories update', error);
- root.visible = !!suggestions.count;
- }, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed.
- allStories.forEach(function (story) {
- counter++;
- filter(story);
- root.visible = !!suggestions.count;
- });
- report('user stories');
- });
- }
- function identity(x) {
- return x;
- }
- function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches
- var words = filter.toUpperCase().split(/\s+/).filter(identity);
- function suggestable(story) {
- // We could filter out places we don't want to suggest, such as those where (story.place_name === AddressManager.placename) or (story.username === Account.username).
- return true;
- }
- function matches(story) {
- if (!words.length) {
- return suggestable(story);
- }
- return words.every(function (word) {
- return story.searchText.indexOf(word) >= 0;
- });
- }
- function addToSuggestions(place) {
- var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement');
- if (collapse) {
- var existing = placeMap[place.place_name];
- if (existing) {
- existing.drillDownToPlace = true;
- return;
- }
- }
- suggestions.append(place);
- if (collapse) {
- placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
- } else if (place.action === 'concurrency') {
- suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
- }
- }
- return function (story) {
- if (matches(story)) {
- addToSuggestions(story);
- }
+ drillDownToPlace: false
};
}
- function filterChoicesByText() {
- suggestions.clear();
- placeMap = {};
- allStories.forEach(makeFilteredStoryProcessor());
- root.visible = !!suggestions.count;
- }
RalewayBold {
id: label;
@@ -208,6 +104,7 @@ Column {
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
currentIndex: -1;
+ onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } }
spacing: 12;
width: parent.width;
@@ -239,21 +136,4 @@ Column {
unhoverThunk: function () { hovered = false }
}
}
- NumberAnimation {
- id: anim;
- target: scroll;
- property: "contentX";
- duration: 250;
- }
- function scrollToIndex(index) {
- anim.running = false;
- var pos = scroll.contentX;
- var destPos;
- scroll.positionViewAtIndex(index, ListView.Contain);
- destPos = scroll.contentX;
- anim.from = pos;
- anim.to = destPos;
- scroll.currentIndex = index;
- anim.running = true;
- }
}
diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml
index d779b4ba42..8dcb76442b 100644
--- a/interface/resources/qml/hifi/Pal.qml
+++ b/interface/resources/qml/hifi/Pal.qml
@@ -18,6 +18,7 @@ import Qt.labs.settings 1.0
import "../styles-uit"
import "../controls-uit" as HifiControlsUit
import "../controls" as HifiControls
+import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
// references HMD, Users, UserActivityLogger from root context
@@ -37,13 +38,42 @@ Rectangle {
property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities.
- property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities.
property bool iAmAdmin: false;
property var activeTab: "nearbyTab";
property bool currentlyEditingDisplayName: false
property bool punctuationMode: false;
HifiConstants { id: hifi; }
+ RootHttpRequest { id: http; }
+ HifiModels.PSFListModel {
+ id: connectionsUserModel;
+ http: http;
+ endpoint: "/api/v1/users?filter=connections";
+ property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn);
+ sortProperty: switch (sortColumn && sortColumn.role) {
+ case 'placeName':
+ 'location';
+ break;
+ case 'connection':
+ 'is_friend';
+ break;
+ default:
+ 'username';
+ }
+ sortAscending: connectionsTable.sortIndicatorOrder === Qt.AscendingOrder;
+ itemsPerPage: 9;
+ listView: connectionsTable;
+ processPage: function (data) {
+ return data.users.map(function (user) {
+ return {
+ userName: user.username,
+ connection: user.connection,
+ profileUrl: user.images.thumbnail,
+ placeName: (user.location.root || user.location.domain || {}).name || ''
+ };
+ });
+ };
+ }
// The letterbox used for popup messages
LetterboxMessage {
@@ -106,16 +136,6 @@ Rectangle {
});
return sessionIDs;
}
- function getSelectedConnectionsUserNames() {
- var userNames = [];
- connectionsTable.selection.forEach(function (userIndex) {
- var datum = connectionsUserModelData[userIndex];
- if (datum) {
- userNames.push(datum.userName);
- }
- });
- return userNames;
- }
function refreshNearbyWithFilter() {
// We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving.
var userIds = getSelectedNearbySessionIDs();
@@ -232,9 +252,7 @@ Rectangle {
anchors.fill: parent;
onClicked: {
if (activeTab != "connectionsTab") {
- connectionsLoading.visible = false;
- connectionsLoading.visible = true;
- pal.sendToScript({method: 'refreshConnections'});
+ connectionsUserModel.getFirstPage();
}
activeTab = "connectionsTab";
connectionsHelpText.color = hifi.colors.blueAccent;
@@ -258,11 +276,7 @@ Rectangle {
id: reloadConnections;
width: reloadConnections.height;
glyph: hifi.glyphs.reload;
- onClicked: {
- connectionsLoading.visible = false;
- connectionsLoading.visible = true;
- pal.sendToScript({method: 'refreshConnections'});
- }
+ onClicked: connectionsUserModel.getFirstPage('delayRefresh');
}
}
// "CONNECTIONS" text
@@ -472,7 +486,7 @@ Rectangle {
visible: !isCheckBox && !isButton && !isAvgAudio;
uuid: model ? model.sessionId : "";
selected: styleData.selected;
- isReplicated: model.isReplicated;
+ isReplicated: model && model.isReplicated;
isAdmin: model && model.admin;
isPresent: model && model.isPresent;
// Size
@@ -702,7 +716,7 @@ Rectangle {
anchors.top: parent.top;
anchors.topMargin: 185;
anchors.horizontalCenter: parent.horizontalCenter;
- visible: true;
+ visible: !connectionsUserModel.retrievedAtLeastOnePage;
onVisibleChanged: {
if (visible) {
connectionsTimeoutTimer.start();
@@ -747,14 +761,6 @@ Rectangle {
headerVisible: true;
sortIndicatorColumn: settings.connectionsSortIndicatorColumn;
sortIndicatorOrder: settings.connectionsSortIndicatorOrder;
- onSortIndicatorColumnChanged: {
- settings.connectionsSortIndicatorColumn = sortIndicatorColumn;
- sortConnectionsModel();
- }
- onSortIndicatorOrderChanged: {
- settings.connectionsSortIndicatorOrder = sortIndicatorOrder;
- sortConnectionsModel();
- }
TableViewColumn {
id: connectionsUserNameHeader;
@@ -779,8 +785,14 @@ Rectangle {
resizable: false;
}
- model: ListModel {
- id: connectionsUserModel;
+ model: connectionsUserModel;
+ Connections {
+ target: connectionsTable.flickableItem;
+ onAtYEndChanged: {
+ if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) {
+ connectionsUserModel.getNextPage();
+ }
+ }
}
// This Rectangle refers to each Row in the connectionsTable.
@@ -859,12 +871,9 @@ Rectangle {
checked: model && (model.connection === "friend");
boxSize: 24;
onClicked: {
- var newValue = model.connection !== "friend";
- connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection"));
- connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming
- pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName});
+ pal.sendToScript({method: checked ? 'addFriend' : 'removeFriend', params: model.userName});
- UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId);
+ UserActivityLogger["palAction"](checked ? styleData.role : "un-" + styleData.role, model.sessionId);
}
}
}
@@ -1130,16 +1139,6 @@ Rectangle {
sortModel();
reloadNearby.color = 0;
break;
- case 'connections':
- var data = message.params;
- if (pal.debug) {
- console.log('Got connection data: ', JSON.stringify(data));
- }
- connectionsUserModelData = data;
- sortConnectionsModel();
- connectionsLoading.visible = false;
- connectionsRefreshProblemText.visible = false;
- break;
case 'select':
var sessionIds = message.params[0];
var selected = message.params[1];
@@ -1239,6 +1238,14 @@ Rectangle {
reloadNearby.color = 2;
}
break;
+ case 'inspectionCertificate_resetCert':
+ // marketplaces.js sends out a signal to QML with that method when the tablet screen changes and it's not changed to a commerce-related screen.
+ // We want it to only be handled by the InspectionCertificate.qml, but there's not an easy way of doing that.
+ // As a part of a "cleanup inspectionCertificate_resetCert" ticket, we'll have to figure out less logspammy way of doing what has to be done.
+ break;
+ case 'http.response':
+ http.handleHttpResponse(message);
+ break;
default:
console.log('Unrecognized message:', JSON.stringify(message));
}
@@ -1287,45 +1294,6 @@ Rectangle {
nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning);
}
}
- function sortConnectionsModel() {
- var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn);
- var sortProperty = column ? column.role : "userName";
- var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1;
- var after = -1 * before;
- // get selection(s) before sorting
- var selectedIDs = getSelectedConnectionsUserNames();
- connectionsUserModelData.sort(function (a, b) {
- var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase();
- if (!aValue && !bValue) {
- return 0;
- } else if (!aValue) {
- return after;
- } else if (!bValue) {
- return before;
- }
- switch (true) {
- case (aValue < bValue): return before;
- case (aValue > bValue): return after;
- default: return 0;
- }
- });
- connectionsTable.selection.clear();
-
- connectionsUserModel.clear();
- var userIndex = 0;
- var newSelectedIndexes = [];
- connectionsUserModelData.forEach(function (datum) {
- datum.userIndex = userIndex++;
- connectionsUserModel.append(datum);
- if (selectedIDs.indexOf(datum.sessionId) != -1) {
- newSelectedIndexes.push(datum.userIndex);
- }
- });
- if (newSelectedIndexes.length > 0) {
- connectionsTable.selection.select(newSelectedIndexes);
- connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning);
- }
- }
signal sendToScript(var message);
function noticeSelection() {
var userIds = [];
diff --git a/interface/resources/qml/hifi/RootHttpRequest.qml b/interface/resources/qml/hifi/RootHttpRequest.qml
new file mode 100644
index 0000000000..0355626996
--- /dev/null
+++ b/interface/resources/qml/hifi/RootHttpRequest.qml
@@ -0,0 +1,39 @@
+//
+// RootHttpRequest.qml
+// qml/hifi
+//
+// Create an item of this in the ROOT qml to be able to make http requests.
+// Used by PSFListModel.qml
+//
+// Created by Howard Stearns on 5/29/2018
+// 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
+
+Item {
+ property var httpCalls: ({});
+ property var httpCounter: 0;
+ // Public function for initiating an http request.
+ // REQUIRES parent to be root to have sendToScript!
+ function request(options, callback) {
+ console.debug('HttpRequest', JSON.stringify(options));
+ httpCalls[httpCounter] = callback;
+ var message = {method: 'http.request', params: options, id: httpCounter++, jsonrpc: "2.0"};
+ parent.sendToScript(message);
+ }
+ // REQUIRES that parent/root handle http.response message.method in fromScript, by calling this function.
+ function handleHttpResponse(message) {
+ var callback = httpCalls[message.id]; // FIXME: as different top level tablet apps gets loaded, the id repeats. We should drop old app callbacks without warning.
+ if (!callback) {
+ console.warn('No callback for', JSON.stringify(message));
+ return;
+ }
+ delete httpCalls[message.id];
+ console.debug('HttpRequest response', JSON.stringify(message));
+ callback(message.error, message.response);
+ }
+}
diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml
index f25282c738..16c1b55930 100644
--- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml
+++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml
@@ -42,7 +42,7 @@ Rectangle {
property bool alreadyOwned: false;
property int itemPrice: -1;
property bool isCertified;
- property string itemType;
+ property string itemType: "unknown";
property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar", "unknown"];
property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar", "item"];
property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR", "REZ"];
@@ -98,9 +98,6 @@ Rectangle {
} else {
root.certificateId = result.data.certificate_id;
root.itemHref = result.data.download_url;
- if (result.data.categories.indexOf("Wearables") > -1) {
- root.itemType = "wearable";
- }
root.activeView = "checkoutSuccess";
UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned);
}
@@ -170,9 +167,6 @@ Rectangle {
root.activeView = "checkoutFailure";
} else {
root.itemHref = result.data.download_url;
- if (result.data.categories.indexOf("Wearables") > -1) {
- root.itemType = "wearable";
- }
root.activeView = "checkoutSuccess";
}
}
@@ -186,20 +180,6 @@ Rectangle {
itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg";
}
- onItemHrefChanged: {
- if (root.itemHref.indexOf(".fst") > -1) {
- root.itemType = "avatar";
- } else if (root.itemHref.indexOf('.json.gz') > -1 || root.itemHref.indexOf('.content.zip') > -1) {
- root.itemType = "contentSet";
- } else if (root.itemHref.indexOf('.app.json') > -1) {
- root.itemType = "app";
- } else if (root.itemHref.indexOf('.json') > -1) {
- root.itemType = "entity"; // "wearable" type handled later
- } else {
- root.itemType = "unknown";
- }
- }
-
onItemTypeChanged: {
if (root.itemType === "entity" || root.itemType === "wearable" ||
root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") {
@@ -1102,6 +1082,7 @@ Rectangle {
root.referrer = message.params.referrer;
root.itemAuthor = message.params.itemAuthor;
root.itemEdition = message.params.itemEdition || -1;
+ root.itemType = message.params.itemType || "unknown";
refreshBuyUI();
break;
default:
diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml
index 66a9f9a822..41eacd68d5 100644
--- a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml
+++ b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml
@@ -44,7 +44,7 @@ Item {
Item {
id: avatarImage;
- visible: profileUrl !== "" && userName !== "";
+ visible: profilePicUrl !== "" && userName !== "";
// Size
anchors.verticalCenter: parent.verticalCenter;
anchors.left: parent.left;
diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml
index c7c72e5f7c..3e4bae4780 100644
--- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml
+++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml
@@ -19,6 +19,7 @@ import "../../../../styles-uit"
import "../../../../controls-uit" as HifiControlsUit
import "../../../../controls" as HifiControls
import "../" as HifiCommerceCommon
+import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
Item {
HifiConstants { id: hifi; }
@@ -36,6 +37,8 @@ Item {
property string assetName: "";
property string assetCertID: "";
property string sendingPubliclyEffectImage;
+ property var http;
+ property var listModelName;
// This object is always used in a popup or full-screen Wallet section.
// This MouseArea is used to prevent a user from being
@@ -118,9 +121,7 @@ Item {
if (root.currentActiveView === 'chooseRecipientConnection') {
// Refresh connections model
- connectionsLoading.visible = false;
- connectionsLoading.visible = true;
- sendSignalToParent({method: 'refreshConnections'});
+ connectionsModel.getFirstPage();
} else if (root.currentActiveView === 'sendAssetHome') {
Commerce.balance();
} else if (root.currentActiveView === 'chooseRecipientNearby') {
@@ -392,11 +393,17 @@ Item {
hoverEnabled: true;
}
- ListModel {
+ HifiModels.PSFListModel {
id: connectionsModel;
- }
- ListModel {
- id: filteredConnectionsModel;
+ http: root.http;
+ listModelName: root.listModelName;
+ endpoint: "/api/v1/users?filter=connections";
+ itemsPerPage: 8;
+ listView: connectionsList;
+ processPage: function (data) {
+ return data.users;
+ };
+ searchFilter: filterBar.text;
}
Rectangle {
@@ -472,10 +479,6 @@ Item {
anchors.fill: parent;
centerPlaceholderGlyph: hifi.glyphs.search;
- onTextChanged: {
- buildFilteredConnectionsModel();
- }
-
onAccepted: {
focus = false;
}
@@ -495,6 +498,7 @@ Item {
AnimatedImage {
id: connectionsLoading;
+ visible: !connectionsModel.retrievedAtLeastOnePage;
source: "../../../../../icons/profilePicLoading.gif"
width: 120;
height: width;
@@ -515,14 +519,15 @@ Item {
}
visible: !connectionsLoading.visible;
clip: true;
- model: filteredConnectionsModel;
+ model: connectionsModel;
+ onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); }
snapMode: ListView.SnapToItem;
// Anchors
anchors.fill: parent;
delegate: ConnectionItem {
isSelected: connectionsList.currentIndex === index;
- userName: model.userName;
- profilePicUrl: model.profileUrl;
+ userName: model.username;
+ profilePicUrl: model.images.thumbnail;
anchors.topMargin: 6;
anchors.bottomMargin: 6;
@@ -553,7 +558,7 @@ Item {
// "Make a Connection" instructions
Rectangle {
id: connectionInstructions;
- visible: connectionsModel.count === 0 && !connectionsLoading.visible;
+ visible: connectionsModel.count === 0 && !connectionsModel.searchFilter && !connectionsLoading.visible;
anchors.fill: parent;
color: "white";
@@ -1806,22 +1811,6 @@ Item {
// FUNCTION DEFINITIONS START
//
- function updateConnections(connections) {
- connectionsModel.clear();
- connectionsModel.append(connections);
- buildFilteredConnectionsModel();
- connectionsLoading.visible = false;
- }
-
- function buildFilteredConnectionsModel() {
- filteredConnectionsModel.clear();
- for (var i = 0; i < connectionsModel.count; i++) {
- if (connectionsModel.get(i).userName.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) {
- filteredConnectionsModel.append(connectionsModel.get(i));
- }
- }
- }
-
function resetSendAssetData() {
amountTextField.focus = false;
optionalMessage.focus = false;
diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
index 8fe1ebe6c9..0d2acf4ec3 100644
--- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
@@ -16,10 +16,12 @@ import QtQuick 2.5
import "../../../styles-uit"
import "../../../controls-uit" as HifiControlsUit
import "../../../controls" as HifiControls
+import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
import "../wallet" as HifiWallet
import "../common" as HifiCommerceCommon
import "../inspectionCertificate" as HifiInspectionCertificate
import "../common/sendAsset" as HifiSendAsset
+import "../.." as HifiCommon
// references XXX from root context
@@ -34,12 +36,17 @@ Rectangle {
property bool punctuationMode: false;
property bool isShowingMyItems: false;
property bool isDebuggingFirstUseTutorial: false;
- property int pendingItemCount: 0;
property string installedApps;
property bool keyboardRaised: false;
property int numUpdatesAvailable: 0;
// Style
color: hifi.colors.white;
+ function getPurchases() {
+ root.activeView = "purchasesMain";
+ root.installedApps = Commerce.getInstalledApps();
+ purchasesModel.getFirstPage();
+ Commerce.getAvailableUpdates();
+ }
Connections {
target: Commerce;
@@ -62,10 +69,7 @@ Rectangle {
if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") {
root.activeView = "firstUseTutorial";
} else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") {
- root.activeView = "purchasesMain";
- root.installedApps = Commerce.getInstalledApps();
- Commerce.inventory();
- Commerce.getAvailableUpdates();
+ getPurchases();
}
} else {
console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus);
@@ -81,39 +85,7 @@ Rectangle {
}
onInventoryResult: {
- purchasesReceived = true;
-
- if (result.status !== 'success') {
- console.log("Failed to get purchases", result.message);
- } else if (!purchasesContentsList.dragging) { // Don't modify the view if the user's scrolling
- var inventoryResult = processInventoryResult(result.data.assets);
-
- var currentIndex = purchasesContentsList.currentIndex === -1 ? 0 : purchasesContentsList.currentIndex;
- purchasesModel.clear();
- purchasesModel.append(inventoryResult);
-
- root.pendingItemCount = 0;
- for (var i = 0; i < purchasesModel.count; i++) {
- if (purchasesModel.get(i).status === "pending") {
- root.pendingItemCount++;
- }
- }
-
- if (previousPurchasesModel.count !== 0) {
- checkIfAnyItemStatusChanged();
- } else {
- // Fill statusChanged default value
- // Not doing this results in the default being true...
- for (var i = 0; i < purchasesModel.count; i++) {
- purchasesModel.setProperty(i, "statusChanged", false);
- }
- }
- previousPurchasesModel.append(inventoryResult);
-
- buildFilteredPurchasesModel();
-
- purchasesContentsList.positionViewAtIndex(currentIndex, ListView.Beginning);
- }
+ purchasesModel.handlePage(result.status !== "success" && result.message, result);
}
onAvailableUpdatesResult: {
@@ -134,6 +106,10 @@ Rectangle {
}
}
+ onIsShowingMyItemsChanged: {
+ getPurchases();
+ }
+
Timer {
id: notSetUpTimer;
interval: 200;
@@ -172,8 +148,14 @@ Rectangle {
}
}
+ HifiCommon.RootHttpRequest {
+ id: http;
+ }
+
HifiSendAsset.SendAsset {
id: sendAsset;
+ http: http;
+ listModelName: "Gift Connections";
z: 998;
visible: root.activeView === "giftAsset";
anchors.fill: parent;
@@ -183,9 +165,7 @@ Rectangle {
Connections {
onSendSignalToParent: {
if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') {
- root.activeView = "purchasesMain";
- Commerce.inventory();
- Commerce.getAvailableUpdates();
+ getPurchases();
} else {
sendToScript(msg);
}
@@ -449,10 +429,7 @@ Rectangle {
case 'tutorial_skipClicked':
case 'tutorial_finished':
Settings.setValue("isFirstUseOfPurchases", false);
- root.activeView = "purchasesMain";
- root.installedApps = Commerce.getInstalledApps();
- Commerce.inventory();
- Commerce.getAvailableUpdates();
+ getPurchases();
break;
}
}
@@ -528,7 +505,7 @@ Rectangle {
},
{
"displayName": "Content Set",
- "filterName": "contentSet"
+ "filterName": "content_set"
},
{
"displayName": "Entity",
@@ -540,7 +517,7 @@ Rectangle {
},
{
"displayName": "Updatable",
- "filterName": "updatable"
+ "filterName": "updated"
}
]
filterBar.primaryFilterChoices.clear();
@@ -548,14 +525,12 @@ Rectangle {
}
onPrimaryFilter_displayNameChanged: {
- buildFilteredPurchasesModel();
- purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
+ purchasesModel.tagsFilter = filterBar.primaryFilter_filterName;
filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName;
}
onTextChanged: {
- buildFilteredPurchasesModel();
- purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
+ purchasesModel.searchFilter = filterBar.text;
filterBar.previousText = filterBar.text;
}
}
@@ -574,24 +549,41 @@ Rectangle {
anchors.topMargin: 16;
}
- ListModel {
+ HifiModels.PSFListModel {
id: purchasesModel;
- }
- ListModel {
- id: previousPurchasesModel;
- }
- HifiCommerceCommon.SortableListModel {
- id: tempPurchasesModel;
- }
- HifiCommerceCommon.SortableListModel {
- id: filteredPurchasesModel;
+ itemsPerPage: 6;
+ listModelName: 'purchases';
+ getPage: function () {
+ console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage);
+ Commerce.inventory(
+ root.isShowingMyItems ? "proofs" : "purchased",
+ filterBar.primaryFilter_filterName,
+ filterBar.text,
+ purchasesModel.currentPageToRetrieve,
+ purchasesModel.itemsPerPage
+ );
+ }
+ processPage: function(data) {
+ purchasesReceived = true; // HRS FIXME?
+ data.assets.forEach(function (item) {
+ if (item.status.length > 1) { console.warn("Unrecognized inventory status", item); }
+ item.status = item.status[0];
+ item.categories = item.categories.join(';');
+ item.cardBackVisible = false;
+ item.isInstalled = root.installedApps.indexOf(item.id) > -1;
+ item.wornEntityID = '';
+ });
+ sendToScript({ method: 'purchases_updateWearables' });
+
+ return data.assets;
+ }
}
ListView {
id: purchasesContentsList;
- visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0);
+ visible: purchasesModel.count !== 0;
clip: true;
- model: filteredPurchasesModel;
+ model: purchasesModel;
snapMode: ListView.SnapToItem;
// Anchors
anchors.top: separator.bottom;
@@ -608,13 +600,13 @@ Rectangle {
itemEdition: model.edition_number;
numberSold: model.number_sold;
limitedRun: model.limited_run;
- displayedItemCount: model.displayedItemCount;
- cardBackVisible: model.cardBackVisible;
- isInstalled: model.isInstalled;
+ displayedItemCount: 999; // For now (and maybe longer), we're going to display all the edition numbers.
+ cardBackVisible: model.cardBackVisible || false;
+ isInstalled: model.isInstalled || false;
wornEntityID: model.wornEntityID;
upgradeUrl: model.upgrade_url;
upgradeTitle: model.upgrade_title;
- itemType: model.itemType;
+ itemType: model.item_type;
isShowingMyItems: root.isShowingMyItems;
valid: model.valid;
anchors.topMargin: 10;
@@ -706,11 +698,11 @@ Rectangle {
} else if (msg.method === "setFilterText") {
filterBar.text = msg.filterText;
} else if (msg.method === "flipCard") {
- for (var i = 0; i < filteredPurchasesModel.count; i++) {
+ for (var i = 0; i < purchasesModel.count; i++) {
if (i !== index || msg.closeAll) {
- filteredPurchasesModel.setProperty(i, "cardBackVisible", false);
+ purchasesModel.setProperty(i, "cardBackVisible", false);
} else {
- filteredPurchasesModel.setProperty(i, "cardBackVisible", true);
+ purchasesModel.setProperty(i, "cardBackVisible", true);
}
}
} else if (msg.method === "updateItemClicked") {
@@ -761,7 +753,7 @@ Rectangle {
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Entities.deleteEntity(msg.wornEntityID);
- filteredPurchasesModel.setProperty(index, 'wornEntityID', '');
+ purchasesModel.setProperty(index, 'wornEntityID', '');
root.activeView = "giftAsset";
lightboxPopup.visible = false;
};
@@ -773,6 +765,14 @@ Rectangle {
}
}
}
+
+
+ onAtYEndChanged: {
+ if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) {
+ console.log("User scrolled to the bottom of 'Purchases'.");
+ purchasesModel.getNextPage();
+ }
+ }
}
Rectangle {
@@ -953,146 +953,14 @@ Rectangle {
//
// FUNCTION DEFINITIONS START
//
-
- function processInventoryResult(inventory) {
- for (var i = 0; i < inventory.length; i++) {
- if (inventory[i].status.length > 1) {
- console.log("WARNING: Inventory result index " + i + " has a status of length >1!")
- }
- inventory[i].status = inventory[i].status[0];
- inventory[i].categories = inventory[i].categories.join(';');
- }
- return inventory;
- }
-
- function populateDisplayedItemCounts() {
- var itemCountDictionary = {};
- var currentItemId;
- for (var i = 0; i < filteredPurchasesModel.count; i++) {
- currentItemId = filteredPurchasesModel.get(i).id;
- if (itemCountDictionary[currentItemId] === undefined) {
- itemCountDictionary[currentItemId] = 1;
- } else {
- itemCountDictionary[currentItemId]++;
- }
- }
-
- for (var i = 0; i < filteredPurchasesModel.count; i++) {
- filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]);
- }
- }
-
- function sortByDate() {
- filteredPurchasesModel.sortColumnName = "purchase_date";
- filteredPurchasesModel.isSortingDescending = true;
- filteredPurchasesModel.valuesAreNumerical = true;
- filteredPurchasesModel.quickSort();
- }
-
- function buildFilteredPurchasesModel() {
- var sameItemCount = 0;
-
- tempPurchasesModel.clear();
-
- for (var i = 0; i < purchasesModel.count; i++) {
- if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) {
- if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) {
- tempPurchasesModel.insert(0, purchasesModel.get(i));
- } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") ||
- (!root.isShowingMyItems && purchasesModel.get(i).edition_number !== "0")) {
- tempPurchasesModel.append(purchasesModel.get(i));
- }
- }
- }
-
- // primaryFilter filtering and adding of itemType property to model
- var currentItemType, currentRootFileUrl, currentCategories;
- for (var i = 0; i < tempPurchasesModel.count; i++) {
- currentRootFileUrl = tempPurchasesModel.get(i).root_file_url;
- currentCategories = tempPurchasesModel.get(i).categories;
-
- if (currentRootFileUrl.indexOf(".fst") > -1) {
- currentItemType = "avatar";
- } else if (currentCategories.indexOf("Wearables") > -1) {
- currentItemType = "wearable";
- } else if (currentRootFileUrl.endsWith('.json.gz') || currentRootFileUrl.endsWith('.content.zip')) {
- currentItemType = "contentSet";
- } else if (currentRootFileUrl.endsWith('.app.json')) {
- currentItemType = "app";
- } else if (currentRootFileUrl.endsWith('.json')) {
- currentItemType = "entity";
- } else {
- currentItemType = "unknown";
- }
- if (filterBar.primaryFilter_displayName !== "" &&
- ((filterBar.primaryFilter_displayName === "Updatable" && tempPurchasesModel.get(i).upgrade_url === "") ||
- (filterBar.primaryFilter_displayName !== "Updatable" && filterBar.primaryFilter_filterName.toLowerCase() !== currentItemType.toLowerCase()))) {
- tempPurchasesModel.remove(i);
- i--;
- } else {
- tempPurchasesModel.setProperty(i, 'itemType', currentItemType);
- }
- }
-
- for (var i = 0; i < tempPurchasesModel.count; i++) {
- if (!filteredPurchasesModel.get(i)) {
- sameItemCount = -1;
- break;
- } else if (tempPurchasesModel.get(i).itemId === filteredPurchasesModel.get(i).itemId &&
- tempPurchasesModel.get(i).edition_number === filteredPurchasesModel.get(i).edition_number &&
- tempPurchasesModel.get(i).status === filteredPurchasesModel.get(i).status) {
- sameItemCount++;
- }
- }
-
- if (sameItemCount !== tempPurchasesModel.count ||
- filterBar.text !== filterBar.previousText ||
- filterBar.primaryFilter !== filterBar.previousPrimaryFilter) {
- filteredPurchasesModel.clear();
- var currentId;
- for (var i = 0; i < tempPurchasesModel.count; i++) {
- currentId = tempPurchasesModel.get(i).id;
- filteredPurchasesModel.append(tempPurchasesModel.get(i));
- filteredPurchasesModel.setProperty(i, 'cardBackVisible', false);
- filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1));
- filteredPurchasesModel.setProperty(i, 'wornEntityID', '');
- }
-
- sendToScript({ method: 'purchases_updateWearables' });
- populateDisplayedItemCounts();
- sortByDate();
- }
- }
-
- function checkIfAnyItemStatusChanged() {
- var currentPurchasesModelId, currentPurchasesModelEdition, currentPurchasesModelStatus;
- var previousPurchasesModelStatus;
- for (var i = 0; i < purchasesModel.count; i++) {
- currentPurchasesModelId = purchasesModel.get(i).id;
- currentPurchasesModelEdition = purchasesModel.get(i).edition_number;
- currentPurchasesModelStatus = purchasesModel.get(i).status;
-
- for (var j = 0; j < previousPurchasesModel.count; j++) {
- previousPurchasesModelStatus = previousPurchasesModel.get(j).status;
- if (currentPurchasesModelId === previousPurchasesModel.get(j).id &&
- currentPurchasesModelEdition === previousPurchasesModel.get(j).edition_number &&
- currentPurchasesModelStatus !== previousPurchasesModelStatus) {
-
- purchasesModel.setProperty(i, "statusChanged", true);
- } else {
- purchasesModel.setProperty(i, "statusChanged", false);
- }
- }
- }
- }
function updateCurrentlyWornWearables(wearables) {
- for (var i = 0; i < filteredPurchasesModel.count; i++) {
+ for (var i = 0; i < purchasesModel.count; i++) {
for (var j = 0; j < wearables.length; j++) {
- if (filteredPurchasesModel.get(i).itemType === "wearable" &&
- wearables[j].entityCertID === filteredPurchasesModel.get(i).certificate_id &&
- wearables[j].entityEdition.toString() === filteredPurchasesModel.get(i).edition_number) {
- filteredPurchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID);
+ if (purchasesModel.get(i).itemType === "wearable" &&
+ wearables[j].entityCertID === purchasesModel.get(i).certificate_id &&
+ wearables[j].entityEdition.toString() === purchasesModel.get(i).edition_number) {
+ purchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID);
break;
}
}
@@ -1149,7 +1017,7 @@ Rectangle {
switch (message.method) {
case 'updatePurchases':
referrerURL = message.referrerURL || "";
- titleBarContainer.referrerURL = message.referrerURL;
+ titleBarContainer.referrerURL = message.referrerURL || "";
filterBar.text = message.filterText ? message.filterText : "";
break;
case 'inspectionCertificate_setCertificateId':
@@ -1168,6 +1036,9 @@ Rectangle {
case 'updateWearables':
updateCurrentlyWornWearables(message.wornWearables);
break;
+ case 'http.response':
+ http.handleHttpResponse(message);
+ break;
default:
console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message));
}
diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml
index 86700f702e..603d7fb676 100644
--- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml
+++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml
@@ -19,6 +19,7 @@ import "../../../controls-uit" as HifiControlsUit
import "../../../controls" as HifiControls
import "../common" as HifiCommerceCommon
import "../common/sendAsset"
+import "../.." as HifiCommon
Rectangle {
HifiConstants { id: hifi; }
@@ -343,8 +344,14 @@ Rectangle {
}
}
+ HifiCommon.RootHttpRequest {
+ id: http;
+ }
+
SendAsset {
id: sendMoney;
+ http: http;
+ listModelName: "Send Money Connections";
z: 997;
visible: root.activeView === "sendMoney";
anchors.fill: parent;
@@ -768,6 +775,13 @@ Rectangle {
case 'updateSelectedRecipientUsername':
sendMoney.fromScript(message);
break;
+ case 'http.response':
+ http.handleHttpResponse(message);
+ break;
+ case 'palIsStale':
+ case 'avatarDisconnected':
+ // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs.
+ break;
default:
console.log('Unrecognized message from wallet.js:', JSON.stringify(message));
}
diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml
index 7a14ee060f..4cf6db7889 100644
--- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml
+++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml
@@ -18,27 +18,17 @@ import QtQuick.Controls 2.2
import "../../../styles-uit"
import "../../../controls-uit" as HifiControlsUit
import "../../../controls" as HifiControls
+import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
Item {
HifiConstants { id: hifi; }
id: root;
- property bool initialHistoryReceived: false;
- property bool historyRequestPending: true;
- property bool noMoreHistoryData: false;
- property int pendingCount: 0;
- property int currentHistoryPage: 1;
- property var pagesAlreadyAdded: new Array();
onVisibleChanged: {
if (visible) {
- transactionHistoryModel.clear();
Commerce.balance();
- initialHistoryReceived = false;
- root.currentHistoryPage = 1;
- root.noMoreHistoryData = false;
- root.historyRequestPending = true;
- Commerce.history(root.currentHistoryPage);
+ transactionHistoryModel.getFirstPage();
Commerce.getAvailableUpdates();
} else {
refreshTimer.stop();
@@ -53,86 +43,7 @@ Item {
}
onHistoryResult : {
- root.initialHistoryReceived = true;
- root.historyRequestPending = false;
-
- if (result.status === 'success') {
- var currentPage = parseInt(result.current_page);
-
- if (result.data.history.length === 0) {
- root.noMoreHistoryData = true;
- console.log("No more data to retrieve from Commerce.history() endpoint.")
- } else if (root.currentHistoryPage === 1) {
- var sameItemCount = 0;
- tempTransactionHistoryModel.clear();
-
- tempTransactionHistoryModel.append(result.data.history);
-
- for (var i = 0; i < tempTransactionHistoryModel.count; i++) {
- if (!transactionHistoryModel.get(i)) {
- sameItemCount = -1;
- break;
- } else if (tempTransactionHistoryModel.get(i).transaction_type === transactionHistoryModel.get(i).transaction_type &&
- tempTransactionHistoryModel.get(i).text === transactionHistoryModel.get(i).text) {
- sameItemCount++;
- }
- }
-
- if (sameItemCount !== tempTransactionHistoryModel.count) {
- transactionHistoryModel.clear();
- for (var i = 0; i < tempTransactionHistoryModel.count; i++) {
- transactionHistoryModel.append(tempTransactionHistoryModel.get(i));
- }
- calculatePendingAndInvalidated();
- }
- } else {
- if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) {
- console.log("Page " + currentPage + " of history has already been added to the list.");
- } else {
- // First, add the history result to a temporary model
- tempTransactionHistoryModel.clear();
- tempTransactionHistoryModel.append(result.data.history);
-
- // Make a note that we've already added this page to the model...
- root.pagesAlreadyAdded.push(currentPage);
-
- var insertionIndex = 0;
- // If there's nothing in the model right now, we don't need to modify insertionIndex.
- if (transactionHistoryModel.count !== 0) {
- var currentIteratorPage;
- // Search through the whole transactionHistoryModel and look for the insertion point.
- // The insertion point is found when the result page from the server is less than
- // the page that the current item came from, OR when we've reached the end of the whole model.
- for (var i = 0; i < transactionHistoryModel.count; i++) {
- currentIteratorPage = transactionHistoryModel.get(i).resultIsFromPage;
-
- if (currentPage < currentIteratorPage) {
- insertionIndex = i;
- break;
- } else if (i === transactionHistoryModel.count - 1) {
- insertionIndex = i + 1;
- break;
- }
- }
- }
-
- // Go through the results we just got back from the server, setting the "resultIsFromPage"
- // property of those results and adding them to the main model.
- for (var i = 0; i < tempTransactionHistoryModel.count; i++) {
- tempTransactionHistoryModel.setProperty(i, "resultIsFromPage", currentPage);
- transactionHistoryModel.insert(i + insertionIndex, tempTransactionHistoryModel.get(i))
- }
-
- calculatePendingAndInvalidated();
- }
- }
- }
-
- // Only auto-refresh if the user hasn't scrolled
- // and there is more data to grab
- if (transactionHistory.atYBeginning && !root.noMoreHistoryData) {
- refreshTimer.start();
- }
+ transactionHistoryModel.handlePage(null, result);
}
onAvailableUpdatesResult: {
@@ -147,7 +58,7 @@ Item {
Connections {
target: GlobalServices
onMyUsernameChanged: {
- transactionHistoryModel.clear();
+ transactionHistoryModel.resetModel();
usernameText.text = Account.username;
}
}
@@ -235,9 +146,8 @@ Item {
onTriggered: {
if (transactionHistory.atYBeginning) {
console.log("Refreshing 1st Page of Recent Activity...");
- root.historyRequestPending = true;
Commerce.balance();
- Commerce.history(1);
+ transactionHistoryModel.getFirstPage("delayedClear");
}
}
}
@@ -299,11 +209,42 @@ Item {
}
}
- ListModel {
- id: tempTransactionHistoryModel;
- }
- ListModel {
+ HifiModels.PSFListModel {
id: transactionHistoryModel;
+ listModelName: "transaction history"; // For debugging. Alternatively, we could specify endpoint for that purpose, even though it's not used directly.
+ itemsPerPage: 6;
+ getPage: function () {
+ console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve);
+ Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage);
+ }
+ processPage: function (data) {
+ console.debug('processPage', transactionHistoryModel.listModelName, JSON.stringify(data));
+ var result, pending; // Set up or get the accumulator for pending.
+ if (transactionHistoryModel.currentPageToRetrieve == 1) {
+ pending = {transaction_type: "pendingCount", count: 0};
+ result = [pending];
+ } else {
+ pending = transactionHistoryModel.get(0);
+ result = [];
+ }
+
+ // Either add to pending, or to result.
+ // Note that you only see a page of pending stuff until you scroll...
+ data.history.forEach(function (item) {
+ if (item.status === 'pending') {
+ pending.count++;
+ } else {
+ result = result.concat(item);
+ }
+ });
+
+ // Only auto-refresh if the user hasn't scrolled
+ // and there is more data to grab
+ if (transactionHistory.atYBeginning && data.history.length) {
+ refreshTimer.start();
+ }
+ return result;
+ }
}
Item {
anchors.top: recentActivityText.bottom;
@@ -312,8 +253,8 @@ Item {
anchors.left: parent.left;
anchors.right: parent.right;
- Item {
- visible: transactionHistoryModel.count === 0 && root.initialHistoryReceived;
+ Item { // On empty history. We don't want to flash and then replace, so don't show until we know we're zero.
+ visible: transactionHistoryModel.count === 0 && transactionHistoryModel.currentPageToRetrieve < 0;
anchors.centerIn: parent;
width: parent.width - 12;
height: parent.height;
@@ -385,10 +326,10 @@ Item {
model: transactionHistoryModel;
delegate: Item {
width: parent.width;
- height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0);
+ height: (model.transaction_type === "pendingCount" && model.count !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0);
Item {
- visible: model.transaction_type === "pendingCount" && root.pendingCount !== 0;
+ visible: model.transaction_type === "pendingCount" && model.count !== 0;
anchors.top: parent.top;
anchors.left: parent.left;
width: parent.width;
@@ -397,7 +338,7 @@ Item {
AnonymousProRegular {
id: pendingCountText;
anchors.fill: parent;
- text: root.pendingCount + ' Transaction' + (root.pendingCount > 1 ? 's' : '') + ' Pending';
+ text: model.count + ' Transaction' + (model.count > 1 ? 's' : '') + ' Pending';
size: 18;
color: hifi.colors.blueAccent;
verticalAlignment: Text.AlignVCenter;
@@ -460,14 +401,9 @@ Item {
}
}
onAtYEndChanged: {
- if (transactionHistory.atYEnd) {
+ if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) {
console.log("User scrolled to the bottom of 'Recent Activity'.");
- if (!root.historyRequestPending && !root.noMoreHistoryData) {
- // Grab next page of results and append to model
- root.historyRequestPending = true;
- Commerce.history(++root.currentHistoryPage);
- console.log("Fetching Page " + root.currentHistoryPage + " of Recent Activity...");
- }
+ transactionHistoryModel.getNextPage();
}
}
}
@@ -506,40 +442,6 @@ Item {
return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm;
}
-
- function calculatePendingAndInvalidated(startingPendingCount) {
- var pendingCount = startingPendingCount ? startingPendingCount : 0;
- for (var i = 0; i < transactionHistoryModel.count; i++) {
- if (transactionHistoryModel.get(i).status === "pending") {
- pendingCount++;
- }
- }
-
- root.pendingCount = pendingCount;
- if (pendingCount > 0) {
- transactionHistoryModel.insert(0, {"transaction_type": "pendingCount"});
- }
- }
-
- //
- // Function Name: fromScript()
- //
- // Relevant Variables:
- // None
- //
- // Arguments:
- // message: The message sent from the JavaScript.
- // Messages are in format "{method, params}", like json-rpc.
- //
- // Description:
- // Called when a message is received from a script.
- //
- function fromScript(message) {
- switch (message.method) {
- default:
- console.log('Unrecognized message from wallet.js:', JSON.stringify(message));
- }
- }
signal sendSignalToWallet(var msg);
//
diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml
new file mode 100644
index 0000000000..1bfa2f6ae0
--- /dev/null
+++ b/interface/resources/qml/hifi/models/PSFListModel.qml
@@ -0,0 +1,168 @@
+//
+// PSFListModel.qml
+// qml/hifi/commerce/common
+//
+// PSFListModel
+// "PSF" stands for:
+// - Paged
+// - Sortable
+// - Filterable
+//
+// Created by Zach Fox on 2018-05-15
+// 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.7
+
+ListModel {
+ id: root;
+ // Used when printing debug statements
+ property string listModelName: endpoint;
+
+ // Parameters. Even if you override getPage, below, please set these for clarity and consistency, when applicable.
+ // E.g., your getPage function could refer to this sortKey, etc.
+ property string endpoint;
+ property string sortProperty; // Currently only handles sorting on one column, which fits with current needs and tables.
+ property bool sortAscending;
+ property string sortKey: !sortProperty ? '' : (sortProperty + "," + (sortAscending ? "ASC" : "DESC"));
+ property string searchFilter: "";
+ property string tagsFilter;
+
+ // QML fires the following changed handlers even when first instantiating the Item. So we need a guard against firing them too early.
+ property bool initialized: false;
+ Component.onCompleted: initialized = true;
+ onEndpointChanged: if (initialized) { getFirstPage('delayClear'); }
+ onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); }
+ onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); }
+ onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); }
+
+ property int itemsPerPage: 100;
+
+ // State.
+ property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number.
+ property bool retrievedAtLeastOnePage: false;
+ // We normally clear on reset. But if we want to "refresh", we can delay clearing the model until we get a result.
+ // Not normally set directly, but rather by giving a truthy argument to getFirstPage(true);
+ property bool delayedClear: false;
+ function resetModel() {
+ if (!delayedClear) { root.clear(); }
+ currentPageToRetrieve = 1;
+ retrievedAtLeastOnePage = false;
+ }
+
+ // Page processing.
+
+ // Override to return one property of data, and/or to transform the elements. Must return an array of model elements.
+ property var processPage: function (data) { return data; }
+
+ property var listView; // Optional. For debugging.
+ // Check consistency and call processPage.
+ function handlePage(error, response) {
+ var processed;
+ console.debug('handlePage', listModelName, additionalFirstPageRequested, error, JSON.stringify(response));
+ function fail(message) {
+ console.warn("Warning page fail", listModelName, JSON.stringify(message));
+ currentPageToRetrieve = -1;
+ requestPending = false;
+ delayedClear = false;
+ }
+ if (error || (response.status !== 'success')) {
+ return fail(error || response.status);
+ }
+ if (!requestPending) {
+ return fail("No request in flight.");
+ }
+ requestPending = false;
+ if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property.
+ return fail("Mismatched page, expected:" + currentPageToRetrieve);
+ }
+ processed = processPage(response.data || response);
+ if (response.total_pages && (response.total_pages === currentPageToRetrieve)) {
+ currentPageToRetrieve = -1;
+ }
+
+ if (delayedClear) {
+ root.clear();
+ delayedClear = false;
+ }
+ root.append(processed); // FIXME keep index steady, and apply any post sort
+ retrievedAtLeastOnePage = true;
+ // Suppose two properties change at once, and both of their change handlers request a new first page.
+ // (An example is when the a filter box gets cleared with text in it, so that the search and tags are both reset.)
+ // Or suppose someone just types new search text quicker than the server response.
+ // In these cases, we would have multiple requests in flight, and signal based responses aren't generally very good
+ // at matching up the right handler with the right message. Rather than require all the APIs to carefully handle such,
+ // and also to cut down on useless requests, we take care of that case here.
+ if (additionalFirstPageRequested) {
+ console.debug('deferred getFirstPage', listModelName);
+ additionalFirstPageRequested = false;
+ getFirstPage('delayedClear');
+ }
+ }
+ function debugView(label) {
+ if (!listView) { return; }
+ console.debug(label, listModelName, 'perPage:', itemsPerPage, 'count:', listView.count,
+ 'index:', listView.currentIndex, 'section:', listView.currentSection,
+ 'atYBeginning:', listView.atYBeginning, 'atYEnd:', listView.atYEnd,
+ 'y:', listView.y, 'contentY:', listView.contentY);
+ }
+
+ // Override either http or getPage.
+ property var http; // An Item that has a request function.
+ property var getPage: function () { // Any override MUST call handlePage(), above, even if results empty.
+ if (!http) { return console.warn("Neither http nor getPage was set for", listModelName); }
+ // If it is a path starting with slash, add the metaverseServer domain.
+ var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint;
+ var parameters = [
+ 'per_page=' + itemsPerPage,
+ 'page=' + currentPageToRetrieve
+ ];
+ if (searchFilter) {
+ parameters.splice(parameters.length, 0, 'search=' + searchFilter);
+ }
+ if (sortKey) {
+ parameters.splice(parameters.length, 0, 'sort=' + sortKey);
+ }
+
+ var parametersSeparator = /\?/.test(url) ? '&' : '?';
+ url = url + parametersSeparator + parameters.join('&');
+ console.debug('getPage', listModelName, currentPageToRetrieve);
+ http.request({uri: url}, handlePage);
+ }
+
+ // Start the show by retrieving data according to `getPage()`.
+ // It can be custom-defined by this item's Parent.
+ property var getFirstPage: function (delayClear) {
+ if (requestPending) {
+ console.debug('deferring getFirstPage', listModelName);
+ additionalFirstPageRequested = true;
+ return;
+ }
+ delayedClear = !!delayClear;
+ resetModel();
+ requestPending = true;
+ console.debug("getFirstPage", listModelName, currentPageToRetrieve);
+ getPage();
+ }
+ property bool additionalFirstPageRequested: false;
+ property bool requestPending: false; // For de-bouncing getNextPage.
+ // This function, will get the _next_ page of data according to `getPage()`.
+ // It can be custom-defined by this item's Parent. Typical usage:
+ // ListView {
+ // id: theList
+ // model: thisPSFListModelId
+ // onAtYEndChanged: if (theList.atYEnd && !theList.atYBeginning) { thisPSFListModelId.getNextPage(); }
+ // ...}
+ property var getNextPage: function () {
+ if (requestPending || currentPageToRetrieve < 0) {
+ return;
+ }
+ currentPageToRetrieve++;
+ console.debug("getNextPage", listModelName, currentPageToRetrieve);
+ requestPending = true;
+ getPage();
+ }
+}
\ No newline at end of file
diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml
index dc67494e27..08f86770e6 100644
--- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml
+++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml
@@ -34,41 +34,16 @@ StackView {
height: parent !== null ? parent.height : undefined
property int cardWidth: 212;
property int cardHeight: 152;
- property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property var tablet: null;
- // This version only implements rpc(method, parameters, callback(error, result)) calls initiated from here, not initiated from .js, nor "notifications".
- property var rpcCalls: ({});
- property var rpcCounter: 0;
+ RootHttpRequest { id: http; }
signal sendToScript(var message);
- function rpc(method, parameters, callback) {
- console.debug('TabletAddressDialog: rpc: method = ', method, 'parameters = ', parameters, 'callback = ', callback)
-
- rpcCalls[rpcCounter] = callback;
- var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"};
- sendToScript(message);
- }
function fromScript(message) {
- if (message.method === 'refreshFeeds') {
- var feeds = [happeningNow, places, snapshots];
- console.debug('TabletAddressDialog::fromScript: refreshFeeds', 'feeds = ', feeds);
-
- feeds.forEach(function(feed) {
- feed.protocol = encodeURIComponent(message.protocolSignature);
- Qt.callLater(feed.fillDestinations);
- });
-
- return;
+ switch (message.method) {
+ case 'http.response':
+ http.handleHttpResponse(message);
+ break;
}
-
- var callback = rpcCalls[message.id];
- if (!callback) {
- // FIXME: We often recieve very long messages here, the logging of which is drastically slowing down the main thread
- //console.log('No callback for message fromScript', JSON.stringify(message));
- return;
- }
- delete rpcCalls[message.id];
- callback(message.error, message.result);
}
Component { id: tabletWebView; TabletWebView {} }
@@ -346,12 +321,11 @@ StackView {
width: parent.width;
cardWidth: 312 + (2 * 4);
cardHeight: 163 + (2 * 4);
- metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'HAPPENING NOW';
actions: 'announcement';
filter: addressLine.text;
goFunction: goCard;
- rpc: root.rpc;
+ http: http;
}
Feed {
id: places;
@@ -359,12 +333,11 @@ StackView {
cardWidth: 210;
cardHeight: 110 + messageHeight;
messageHeight: 44;
- metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'PLACES';
actions: 'concurrency';
filter: addressLine.text;
goFunction: goCard;
- rpc: root.rpc;
+ http: http;
}
Feed {
id: snapshots;
@@ -373,12 +346,11 @@ StackView {
cardHeight: 75 + messageHeight + 4;
messageHeight: 32;
textPadding: 6;
- metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'RECENT SNAPS';
actions: 'snapshot';
filter: addressLine.text;
goFunction: goCard;
- rpc: root.rpc;
+ http: http;
}
}
}
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 62b2486555..c5857dac53 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -697,8 +697,8 @@ private:
};
/**jsdoc
- *
The Controller.Hardware.Application
object has properties representing Interface's state. The property
- * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or
+ *
The Controller.Hardware.Application
object has properties representing Interface's state. The property
+ * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or
* Controller.Standard
items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method).
* Each data value is either 1.0
for "true" or 0.0
for "false".