From ccab2dd641b0a27b9ad29aa889af97e2154fa6ac Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 May 2018 11:48:33 -0700 Subject: [PATCH 01/32] Starting the work --- .../qml/hifi/commerce/wallet/WalletHome.qml | 109 ++-------- .../qml/hifi/models/PSFListModel.qml | 188 ++++++++++++++++++ 2 files changed, 207 insertions(+), 90 deletions(-) create mode 100644 interface/resources/qml/hifi/models/PSFListModel.qml diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 7a14ee060f..afc205fd51 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -18,27 +18,24 @@ import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "../../../models" as HifiModels Item { HifiConstants { id: hifi; } id: root; - property bool initialHistoryReceived: false; - property bool historyRequestPending: true; - property bool noMoreHistoryData: false; + property bool initialResultReceived: 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.initialResultReceived = false; + transactionHistoryModel.nextPageToRetrieve = 1; + transactionHistoryModel.noMoreDataToRetrieve = false; + transactionHistoryModel.requestPending = true; + Commerce.history(transactionHistoryModel.nextPageToRetrieve); Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); @@ -53,84 +50,16 @@ Item { } onHistoryResult : { - root.initialHistoryReceived = true; - root.historyRequestPending = false; + + transactionHistoryModel.processResult(result.status, result.data.history); - 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(); - } - } + if (!transactionHistoryModel.noMoreDataToRetrieve) { + calculatePendingAndInvalidated(); } // Only auto-refresh if the user hasn't scrolled // and there is more data to grab - if (transactionHistory.atYBeginning && !root.noMoreHistoryData) { + if (transactionHistory.atYBeginning && !transactionHistoryModel.noMoreDataToRetrieve) { refreshTimer.start(); } } @@ -235,7 +164,7 @@ Item { onTriggered: { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); - root.historyRequestPending = true; + transactionHistoryModel.requestPending = true; Commerce.balance(); Commerce.history(1); } @@ -302,7 +231,7 @@ Item { ListModel { id: tempTransactionHistoryModel; } - ListModel { + HifiModels.PSFListModel { id: transactionHistoryModel; } Item { @@ -313,7 +242,7 @@ Item { anchors.right: parent.right; Item { - visible: transactionHistoryModel.count === 0 && root.initialHistoryReceived; + visible: transactionHistoryModel.count === 0 && transactionHistoryModel.initialResultReceived; anchors.centerIn: parent; width: parent.width - 12; height: parent.height; @@ -462,11 +391,11 @@ Item { onAtYEndChanged: { if (transactionHistory.atYEnd) { console.log("User scrolled to the bottom of 'Recent Activity'."); - if (!root.historyRequestPending && !root.noMoreHistoryData) { + if (!transactionHistoryModel.requestPending && !transactionHistoryModel.noMoreDataToRetrieve) { // 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.requestPending = true; + Commerce.history(++transactionHistoryModel.nextPageToRetrieve); + console.log("Fetching Page " + transactionHistoryModel.nextPageToRetrieve + " of Recent Activity..."); } } } diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml new file mode 100644 index 0000000000..cbc28b208d --- /dev/null +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -0,0 +1,188 @@ +// +// 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; + property string sortColumnName: ""; + property bool isSortingDescending: true; + property bool valuesAreNumerical: false; + + property bool initialResultReceived: false; + property bool requestPending: false; + property bool noMoreDataToRetrieve: false; + property int nextPageToRetrieve: 1; + property var pagesAlreadyAdded: new Array(); + + ListModel { + id: tempModel; + } + + function processResult(status, retrievedResult) { + root.initialResultReceived = true; + root.requestPending = false; + + if (status === 'success') { + var currentPage = parseInt(result.current_page); + + if (retrievedResult.length === 0) { + root.noMoreDataToRetrieve = true; + console.log("No more data to retrieve from backend endpoint.") + } else if (root.nextPageToRetrieve === 1) { + var sameItemCount = 0; + + tempModel.clear(); + tempModel.append(retrievedResult); + + for (var i = 0; i < tempModel.count; i++) { + if (!root.get(i)) { + sameItemCount = -1; + break; + } + // Gotta think of another way to determine if the data we just got is the same + // as the data that we already have in the model. + /* else if (tempModel.get(i).transaction_type === root.get(i).transaction_type && + tempModel.get(i).text === root.get(i).text) { + sameItemCount++; + }*/ + } + + if (sameItemCount !== tempModel.count) { + root.clear(); + for (var i = 0; i < tempModel.count; i++) { + root.append(tempModel.get(i)); + } + } + } else { + if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { + console.log("Page " + currentPage + " of paginated data has already been added to the list."); + } else { + // First, add the result to a temporary model + tempModel.clear(); + tempModel.append(retrievedResult); + + // 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 (root.count !== 0) { + var currentIteratorPage; + // Search through the whole model 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 < root.count; i++) { + currentIteratorPage = root.get(i).resultIsFromPage; + + if (currentPage < currentIteratorPage) { + insertionIndex = i; + break; + } else if (i === root.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. + // NOTE that this wouldn't be necessary if we did this step on the server. + for (var i = 0; i < tempModel.count; i++) { + tempModel.setProperty(i, "resultIsFromPage", currentPage); + root.insert(i + insertionIndex, tempModel.get(i)) + } + } + } + } + } + + function swap(a, b) { + if (a < b) { + move(a, b, 1); + move(b - 1, a, 1); + } else if (a > b) { + move(b, a, 1); + move(a - 1, b, 1); + } + } + + function partition(begin, end, pivot) { + if (valuesAreNumerical) { + var piv = get(pivot)[sortColumnName]; + swap(pivot, end - 1); + var store = begin; + var i; + + for (i = begin; i < end - 1; ++i) { + var currentElement = get(i)[sortColumnName]; + if (isSortingDescending) { + if (currentElement > piv) { + swap(store, i); + ++store; + } + } else { + if (currentElement < piv) { + swap(store, i); + ++store; + } + } + } + swap(end - 1, store); + + return store; + } else { + var piv = get(pivot)[sortColumnName].toLowerCase(); + swap(pivot, end - 1); + var store = begin; + var i; + + for (i = begin; i < end - 1; ++i) { + var currentElement = get(i)[sortColumnName].toLowerCase(); + if (isSortingDescending) { + if (currentElement > piv) { + swap(store, i); + ++store; + } + } else { + if (currentElement < piv) { + swap(store, i); + ++store; + } + } + } + swap(end - 1, store); + + return store; + } + } + + function qsort(begin, end) { + if (end - 1 > begin) { + var pivot = begin + Math.floor(Math.random() * (end - begin)); + + pivot = partition(begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot + 1, end); + } + } + + function quickSort() { + qsort(0, count) + } +} \ No newline at end of file From 154d70866f5261075c83323a322e34d912a52e2d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 16 May 2018 12:17:17 -0700 Subject: [PATCH 02/32] It's working --- .../qml/hifi/commerce/wallet/WalletHome.qml | 52 +++---- .../qml/hifi/models/PSFListModel.qml | 147 +++++++++++++++--- interface/src/commerce/Ledger.cpp | 4 +- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 4 +- interface/src/commerce/QmlCommerce.h | 2 +- 6 files changed, 158 insertions(+), 53 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index afc205fd51..ac3afd3cf2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -18,7 +18,7 @@ import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls -import "../../../models" as HifiModels +import "../../models" as HifiModels Item { HifiConstants { id: hifi; } @@ -32,10 +32,10 @@ Item { transactionHistoryModel.clear(); Commerce.balance(); transactionHistoryModel.initialResultReceived = false; - transactionHistoryModel.nextPageToRetrieve = 1; + transactionHistoryModel.currentPageToRetrieve = 1; transactionHistoryModel.noMoreDataToRetrieve = false; transactionHistoryModel.requestPending = true; - Commerce.history(transactionHistoryModel.nextPageToRetrieve); + transactionHistoryModel.getPage(); Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); @@ -50,18 +50,7 @@ Item { } onHistoryResult : { - - transactionHistoryModel.processResult(result.status, result.data.history); - - if (!transactionHistoryModel.noMoreDataToRetrieve) { - calculatePendingAndInvalidated(); - } - - // Only auto-refresh if the user hasn't scrolled - // and there is more data to grab - if (transactionHistory.atYBeginning && !transactionHistoryModel.noMoreDataToRetrieve) { - refreshTimer.start(); - } + transactionHistoryModel.pageRetrieved(result); } onAvailableUpdatesResult: { @@ -166,7 +155,8 @@ Item { console.log("Refreshing 1st Page of Recent Activity..."); transactionHistoryModel.requestPending = true; Commerce.balance(); - Commerce.history(1); + transactionHistoryModel.currentPageToRetrieve = 1; + transactionHistoryModel.getPage(); } } } @@ -228,11 +218,26 @@ Item { } } - ListModel { - id: tempTransactionHistoryModel; - } HifiModels.PSFListModel { id: transactionHistoryModel; + + itemsPerPage: 100; + getPage: function() { + Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); + } + pageRetrieved: function(result) { + transactionHistoryModel.processResult(result.status, result.data.history); + + if (!transactionHistoryModel.noMoreDataToRetrieve) { + calculatePendingAndInvalidated(); + } + + // Only auto-refresh if the user hasn't scrolled + // and there is more data to grab + if (transactionHistory.atYBeginning && !transactionHistoryModel.noMoreDataToRetrieve) { + refreshTimer.start(); + } + } } Item { anchors.top: recentActivityText.bottom; @@ -311,7 +316,7 @@ Item { height: parent.height; visible: transactionHistoryModel.count !== 0; clip: true; - model: transactionHistoryModel; + model: transactionHistoryModel.model; delegate: Item { width: parent.width; height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); @@ -391,12 +396,7 @@ Item { onAtYEndChanged: { if (transactionHistory.atYEnd) { console.log("User scrolled to the bottom of 'Recent Activity'."); - if (!transactionHistoryModel.requestPending && !transactionHistoryModel.noMoreDataToRetrieve) { - // Grab next page of results and append to model - transactionHistoryModel.requestPending = true; - Commerce.history(++transactionHistoryModel.nextPageToRetrieve); - console.log("Fetching Page " + transactionHistoryModel.nextPageToRetrieve + " of Recent Activity..."); - } + transactionHistoryModel.getNextPage(); } } } diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index cbc28b208d..ee639a4f71 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -17,58 +17,156 @@ import QtQuick 2.7 -ListModel { +Item { id: root; - property string sortColumnName: ""; - property bool isSortingDescending: true; - property bool valuesAreNumerical: false; + + // Used when printing debug statements + property string listModelName: ""; + + // Holds the value of the page that'll be retrieved the next time `getPage()` is called + property int currentPageToRetrieve: 1; + + // If defined, the endpoint that `getPage()` will hit (as long as there isn't a custom `getPage()` + // that does something fancy) + property string endpoint; + // If defined, the sort key used when calling the above endpoint. + // (as long as there isn't a custom `getPage()` that does something fancy) + property string sortKey; + // If defined, the search filter used when calling the above endpoint. + // (as long as there isn't a custom `getPage()` that does something fancy) + property string searchFilter; + // If defined, the tags filter used when calling the above endpoint. + // (as long as there isn't a custom `getPage()` that does something fancy) + property string tagsFilter; + // The number of items that'll be retrieved per page when calling `getPage()` + // (as long as there isn't a custom `getPage()` that does something fancy) + property int itemsPerPage: 100; + + // This function, by default, will retrieve data from the above-defined `endpoint` with the + // sort and filter data as set above. It can be custom-defined by this item's Parent. + property var getPage: function() { + // Put code here that calls the `endpoint` with the proper `sortKey`, `searchFilter`, and `tagsFilter`. + // Whatever code goes here should define the below `pageRetrieved()` as + // the callback for after the page is asynchronously retrieved. + + // The parent of this Item can also define custom `getPage()` and `pageRetrieved()` functions. + // See `WalletHome.qml` as an example of a file that does this. `WalletHome.qml` must use that method because + // it hits an endpoint that must be authenticated via the Wallet. + console.log("default getPage()"); + } + // This function, by default, will handle the data retrieved using `getPage()` above. + // It can be custom-defined by this item's Parent. + property var pageRetrieved: function() { + console.log("default pageRetrieved()"); + } + + // This function, by default, will get the _next_ page of data according to `getPage()` if there + // isn't a pending request and if there's more data to retrieve. + // It can be custom-defined by this item's Parent. + property var getNextPage: function() { + if (!root.requestPending && !root.noMoreDataToRetrieve) { + root.requestPending = true; + root.currentPageToRetrieve++; + root.getPage(); + console.log("Fetching Page " + root.currentPageToRetrieve + " of " + root.listModelName + "..."); + } + } + + // Resets both internal `ListModel`s and resets the page to retrieve to "1". + function resetModel() { + pagesAlreadyAdded = new Array(); + tempModel.clear(); + finalModel.clear(); + root.currentPageToRetrieve = 1; + } + + onEndpointChanged: { + resetModel(); + root.getPage(); + } + + onSortKeyChanged: { + resetModel(); + root.getPage(); + } + + onSearchFilterChanged: { + resetModel(); + root.getPage(); + } + + onTagsFilterChanged: { + resetModel(); + root.getPage(); + } property bool initialResultReceived: false; property bool requestPending: false; property bool noMoreDataToRetrieve: false; - property int nextPageToRetrieve: 1; property var pagesAlreadyAdded: new Array(); + + // Redefining members and methods so that the parent of this Item + // can use PSFListModel as they would a regular ListModel + property alias model: finalModel; + property alias count: finalModel.count; + function clear() { finalModel.clear(); } + function get(index) { return finalModel.get(index); } + // Used while processing page data and sorting ListModel { id: tempModel; } + // This is the model that the parent of this Item will actually see + ListModel { + id: finalModel; + } + function processResult(status, retrievedResult) { root.initialResultReceived = true; root.requestPending = false; if (status === 'success') { - var currentPage = parseInt(result.current_page); + var currentPage = parseInt(retrievedResult.current_page); if (retrievedResult.length === 0) { root.noMoreDataToRetrieve = true; console.log("No more data to retrieve from backend endpoint.") - } else if (root.nextPageToRetrieve === 1) { + } + /* + See FIXME below... + + else if (root.currentPageToRetrieve === 1) { var sameItemCount = 0; tempModel.clear(); tempModel.append(retrievedResult); for (var i = 0; i < tempModel.count; i++) { - if (!root.get(i)) { + if (!finalModel.get(i)) { sameItemCount = -1; break; } - // Gotta think of another way to determine if the data we just got is the same + // Gotta think of a generic way to determine if the data we just got is the same // as the data that we already have in the model. - /* else if (tempModel.get(i).transaction_type === root.get(i).transaction_type && - tempModel.get(i).text === root.get(i).text) { + else if (tempModel.get(i).transaction_type === finalModel.get(i).transaction_type && + tempModel.get(i).text === finalModel.get(i).text) { sameItemCount++; - }*/ + } } if (sameItemCount !== tempModel.count) { - root.clear(); + finalModel.clear(); for (var i = 0; i < tempModel.count; i++) { - root.append(tempModel.get(i)); + finalModel.append(tempModel.get(i)); } } - } else { + } + */ + else { + // FIXME! Reconsider this logic, because it means that auto-refreshing the first page of results + // (like we do in WalletHome for Recent Activity) _won't_ catch brand new data elements! + // See the commented code above for how I did this for WalletHome specifically. if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { console.log("Page " + currentPage + " of paginated data has already been added to the list."); } else { @@ -81,18 +179,18 @@ ListModel { var insertionIndex = 0; // If there's nothing in the model right now, we don't need to modify insertionIndex. - if (root.count !== 0) { + if (finalModel.count !== 0) { var currentIteratorPage; // Search through the whole model 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 < root.count; i++) { - currentIteratorPage = root.get(i).resultIsFromPage; + for (var i = 0; i < finalModel.count; i++) { + currentIteratorPage = finalModel.get(i).resultIsFromPage; if (currentPage < currentIteratorPage) { insertionIndex = i; break; - } else if (i === root.count - 1) { + } else if (i === finalModel.count - 1) { insertionIndex = i + 1; break; } @@ -101,16 +199,23 @@ ListModel { // 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. - // NOTE that this wouldn't be necessary if we did this step on the server. + // NOTE that this wouldn't be necessary if we did this step (or a similar step) on the server. for (var i = 0; i < tempModel.count; i++) { tempModel.setProperty(i, "resultIsFromPage", currentPage); - root.insert(i + insertionIndex, tempModel.get(i)) + finalModel.insert(i + insertionIndex, tempModel.get(i)) } } } } } + // Used when sorting model data on the CLIENT + // Right now, there is no sorting done on the client for + // any users of PSFListModel, but that could very easily change. + property string sortColumnName: ""; + property bool isSortingDescending: true; + property bool valuesAreNumerical: false; + function swap(a, b) { if (a < b) { move(a, b, 1); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index f791ea25bc..0abdba1214 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -260,9 +260,9 @@ void Ledger::historyFailure(QNetworkReply& reply) { failResponse("history", reply); } -void Ledger::history(const QStringList& keys, const int& pageNumber) { +void Ledger::history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage) { QJsonObject params; - params["per_page"] = 100; + params["per_page"] = itemsPerPage; params["page"] = pageNumber; keysQuery("history", "historySuccess", "historyFailure", params); } diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index abc97bfe72..1365e39b21 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -29,7 +29,7 @@ public: bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); void inventory(const QStringList& keys); - void history(const QStringList& keys, const int& pageNumber); + void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); void certificateInfo(const QString& certificateId); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 722f29ba2f..9a5b0519a0 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -114,12 +114,12 @@ void QmlCommerce::inventory() { } } -void QmlCommerce::history(const int& pageNumber) { +void QmlCommerce::history(const int& pageNumber, const int& itemsPerPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->history(cachedPublicKeys, pageNumber); + ledger->history(cachedPublicKeys, pageNumber, itemsPerPage); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 27e97fe7db..5f33ab094c 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -74,7 +74,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); Q_INVOKABLE void inventory(); - Q_INVOKABLE void history(const int& pageNumber); + Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); From 0021af871457278b845116d13f047089d51e6a6d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 16 May 2018 12:28:52 -0700 Subject: [PATCH 03/32] Merge in in-progress changes from old commerce_paginationAndFiltering branch --- .../qml/hifi/commerce/checkout/Checkout.qml | 23 ++----------------- .../qml/hifi/commerce/purchases/Purchases.qml | 21 ++++------------- interface/src/commerce/Ledger.cpp | 9 ++++++-- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 4 ++-- interface/src/commerce/QmlCommerce.h | 2 +- scripts/system/html/js/marketplacesInject.js | 12 ++++++---- 7 files changed, 25 insertions(+), 48 deletions(-) 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/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 8fe1ebe6c9..e32b31c1ea 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -614,7 +614,7 @@ Rectangle { 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; @@ -996,7 +996,8 @@ Rectangle { 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) { + if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems && + purchasesModel.get(i).edition_number !== "0") { tempPurchasesModel.insert(0, purchasesModel.get(i)); } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") || (!root.isShowingMyItems && purchasesModel.get(i).edition_number !== "0")) { @@ -1010,27 +1011,13 @@ Rectangle { for (var i = 0; i < tempPurchasesModel.count; i++) { currentRootFileUrl = tempPurchasesModel.get(i).root_file_url; currentCategories = tempPurchasesModel.get(i).categories; + currentItemType = tempPurchasesModel.get(i).item_type; - 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); } } diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 0abdba1214..5bd8c77d04 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -134,8 +134,13 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QStringList& keys) { - keysQuery("inventory", "inventorySuccess", "inventoryFailure"); +void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage) { + QJsonObject params; + params["edition_filter"] = editionFilter; + params["type_filter"] = typeFilter; + params["page"] = page; + params["per_page"] = perPage; + keysQuery("inventory", "inventorySuccess", "inventoryFailure", params); } QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) { diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 1365e39b21..9733658357 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -28,7 +28,7 @@ public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); - void inventory(const QStringList& keys); + void inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage); void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 9a5b0519a0..dba8cd03c7 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -105,12 +105,12 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory() { +void QmlCommerce::inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(cachedPublicKeys); + ledger->inventory(editionFilter, typeFilter, page, perPage); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 5f33ab094c..3a08b4a19b 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -73,7 +73,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(); + Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const int& page = 1, const int& perPage = 20); Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 864c7d92b4..799a974fd6 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -250,7 +250,7 @@ }); } - function buyButtonClicked(id, name, author, price, href, referrer, edition) { + function buyButtonClicked(id, name, author, price, href, referrer, edition, type) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", itemId: id, @@ -259,7 +259,8 @@ itemHref: href, referrer: referrer, itemAuthor: author, - itemEdition: edition + itemEdition: edition, + itemType: type.trim() })); } @@ -328,7 +329,8 @@ $(this).closest('.grid-item').find('.item-cost').text(), $(this).attr('data-href'), "mainPage", - -1); + -1, + $(this).closest('.grid-item').find('.item-type').text()); }); } @@ -419,6 +421,7 @@ } var cost = $('.item-cost').text(); + var type = $('.item-type').text(); var isUpdating = window.location.href.indexOf('edition=') > -1; var urlParams = new URLSearchParams(window.location.search); if (isUpdating) { @@ -438,7 +441,8 @@ cost, href, "itemPage", - urlParams.get('edition')); + urlParams.get('edition'), + type); } }); maybeAddPurchasesButton(); From 97bb0147348ae9bf8e0e18d8887095955c44945f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 16 May 2018 13:20:23 -0700 Subject: [PATCH 04/32] Almost working for Purchases! --- .../qml/hifi/commerce/purchases/Purchases.qml | 254 +++++++----------- .../qml/hifi/commerce/wallet/WalletHome.qml | 4 +- .../qml/hifi/models/PSFListModel.qml | 8 + 3 files changed, 106 insertions(+), 160 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index e32b31c1ea..9f5dec7ef3 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -16,6 +16,7 @@ import QtQuick 2.5 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "../../models" as HifiModels import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon import "../inspectionCertificate" as HifiInspectionCertificate @@ -34,7 +35,6 @@ 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; @@ -64,7 +64,10 @@ Rectangle { } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); + purchasesModel.initialResultReceived = false; + purchasesModel.currentPageToRetrieve = 1; + purchasesModel.noMoreDataToRetrieve = false; + purchasesModel.getPage(); Commerce.getAvailableUpdates(); } } else { @@ -81,39 +84,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.pageRetrieved(result); } onAvailableUpdatesResult: { @@ -134,6 +105,11 @@ Rectangle { } } + onIsShowingMyItemsChanged: { + purchasesModel.resetModel(); + + } + Timer { id: notSetUpTimer; interval: 200; @@ -184,7 +160,10 @@ Rectangle { onSendSignalToParent: { if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') { root.activeView = "purchasesMain"; - Commerce.inventory(); + purchasesModel.initialResultReceived = false; + purchasesModel.currentPageToRetrieve = 1; + purchasesModel.noMoreDataToRetrieve = false; + purchasesModel.getPage(); Commerce.getAvailableUpdates(); } else { sendToScript(msg); @@ -451,7 +430,10 @@ Rectangle { Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); + purchasesModel.initialResultReceived = false; + purchasesModel.currentPageToRetrieve = 1; + purchasesModel.noMoreDataToRetrieve = false; + purchasesModel.getPage(); Commerce.getAvailableUpdates(); break; } @@ -548,14 +530,12 @@ Rectangle { } onPrimaryFilter_displayNameChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.tagsFilter = filterBar.primaryFilter_filterName.toLowerCase(); filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName; } onTextChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.searchFilter = filterBar.text; filterBar.previousText = filterBar.text; } } @@ -574,24 +554,58 @@ Rectangle { anchors.topMargin: 16; } - ListModel { + HifiModels.PSFListModel { id: purchasesModel; - } - ListModel { - id: previousPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: tempPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: filteredPurchasesModel; + + itemsPerPage: 3; + getPage: function() { + Commerce.inventory( + root.isShowingMyItems ? "proofs" : "purchased", + filterBar.primaryFilter_filterName.toLowerCase(), + purchasesModel.currentPageToRetrieve, + purchasesModel.itemsPerPage + ); + } + pageRetrieved: function(result) { + purchasesReceived = true; + + var processedInventory = processInventoryResult(result.data.assets); + + if (purchasesModel.processResult(result.status, processedInventory)) { + var currentId; + for (var i = 0; i < purchasesModel.count; i++) { + currentId = purchasesModel.get(i).id; + purchasesModel.setProperty(i, 'cardBackVisible', false); + purchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); + purchasesModel.setProperty(i, 'wornEntityID', ''); + } + + // Client-side filter of "Updatable" items + // FIXME - this MUST be serverside (what if we don't have the + // page containing an updatable item on the client?) + if (filterBar.primaryFilter_displayName === "Updatable") { + for (var i = 0; i < purchasesModel.count; i++) { + if (purchasesModel.get(i).upgrade_url === "") { + purchasesModel.remove(i); + i--; + } + } + } + + sendToScript({ method: 'purchases_updateWearables' }); + // FIXME: This ALSO *MUST* be serverside (what if we don't have + // all instances of the item on the client yet?) + //populateDisplayedItemCounts(); + sortByDate(); + } + } } ListView { id: purchasesContentsList; - visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); + visible: purchasesModel.count !== 0; clip: true; - model: filteredPurchasesModel; + model: purchasesModel.model; snapMode: ListView.SnapToItem; // Anchors anchors.top: separator.bottom; @@ -608,9 +622,9 @@ Rectangle { itemEdition: model.edition_number; numberSold: model.number_sold; limitedRun: model.limited_run; - displayedItemCount: model.displayedItemCount; - cardBackVisible: model.cardBackVisible; - isInstalled: model.isInstalled; + displayedItemCount: model.displayedItemCount || 0; + cardBackVisible: model.cardBackVisible || false; + isInstalled: model.isInstalled || false; wornEntityID: model.wornEntityID; upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; @@ -706,11 +720,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 +775,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 +787,14 @@ Rectangle { } } } + + + onAtYEndChanged: { + if (purchasesContentsList.atYEnd) { + console.log("User scrolled to the bottom of 'Purchases'."); + purchasesModel.getNextPage(); + } + } } Rectangle { @@ -968,8 +990,8 @@ Rectangle { function populateDisplayedItemCounts() { var itemCountDictionary = {}; var currentItemId; - for (var i = 0; i < filteredPurchasesModel.count; i++) { - currentItemId = filteredPurchasesModel.get(i).id; + for (var i = 0; i < purchasesModel.count; i++) { + currentItemId = purchasesModel.get(i).id; if (itemCountDictionary[currentItemId] === undefined) { itemCountDictionary[currentItemId] = 1; } else { @@ -977,109 +999,25 @@ Rectangle { } } - for (var i = 0; i < filteredPurchasesModel.count; i++) { - filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]); + for (var i = 0; i < purchasesModel.count; i++) { + purchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[purchasesModel.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 && - purchasesModel.get(i).edition_number !== "0") { - 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; - currentItemType = tempPurchasesModel.get(i).item_type; - - 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--; - } - } - - 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); - } - } - } + purchasesModel.sortColumnName = "purchase_date"; + purchasesModel.isSortingDescending = true; + purchasesModel.valuesAreNumerical = true; + purchasesModel.quickSort(); } 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; } } @@ -1136,7 +1074,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': diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index ac3afd3cf2..b41d04f718 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -34,7 +34,6 @@ Item { transactionHistoryModel.initialResultReceived = false; transactionHistoryModel.currentPageToRetrieve = 1; transactionHistoryModel.noMoreDataToRetrieve = false; - transactionHistoryModel.requestPending = true; transactionHistoryModel.getPage(); Commerce.getAvailableUpdates(); } else { @@ -153,7 +152,6 @@ Item { onTriggered: { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); - transactionHistoryModel.requestPending = true; Commerce.balance(); transactionHistoryModel.currentPageToRetrieve = 1; transactionHistoryModel.getPage(); @@ -221,8 +219,10 @@ Item { HifiModels.PSFListModel { id: transactionHistoryModel; + listModelName: "transaction history"; itemsPerPage: 100; getPage: function() { + transactionHistoryModel.requestPending = true; Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); } pageRetrieved: function(result) { diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index ee639a4f71..41134ddfdb 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -111,6 +111,9 @@ Item { property alias count: finalModel.count; function clear() { finalModel.clear(); } function get(index) { return finalModel.get(index); } + function remove(index) { return finalModel.remove(index); } + function setProperty(index, prop, value) { return finalModel.setProperty(index, prop, value); } + function move(from, to, n) { return finalModel.move(from, to, n); } // Used while processing page data and sorting ListModel { @@ -206,7 +209,12 @@ Item { } } } + return true; + } else { + console.log("Failed to get page result for " + root.listModelName); } + + return false; } // Used when sorting model data on the CLIENT From bc590f556accecdf3faf66ee1d1a7e2d5cd981d3 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 16 May 2018 13:32:53 -0700 Subject: [PATCH 05/32] Get first page --- .../qml/hifi/commerce/purchases/Purchases.qml | 20 +++++++------------ .../qml/hifi/commerce/wallet/WalletHome.qml | 5 +---- .../qml/hifi/models/PSFListModel.qml | 17 ++++++++++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 9f5dec7ef3..93400349a2 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -64,10 +64,7 @@ Rectangle { } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); - purchasesModel.initialResultReceived = false; - purchasesModel.currentPageToRetrieve = 1; - purchasesModel.noMoreDataToRetrieve = false; - purchasesModel.getPage(); + purchasesModel.getFirstPage(); Commerce.getAvailableUpdates(); } } else { @@ -160,10 +157,7 @@ Rectangle { onSendSignalToParent: { if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') { root.activeView = "purchasesMain"; - purchasesModel.initialResultReceived = false; - purchasesModel.currentPageToRetrieve = 1; - purchasesModel.noMoreDataToRetrieve = false; - purchasesModel.getPage(); + purchasesModel.getFirstPage(); Commerce.getAvailableUpdates(); } else { sendToScript(msg); @@ -430,10 +424,7 @@ Rectangle { Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); - purchasesModel.initialResultReceived = false; - purchasesModel.currentPageToRetrieve = 1; - purchasesModel.noMoreDataToRetrieve = false; - purchasesModel.getPage(); + purchasesModel.getFirstPage(); Commerce.getAvailableUpdates(); break; } @@ -596,7 +587,10 @@ Rectangle { // FIXME: This ALSO *MUST* be serverside (what if we don't have // all instances of the item on the client yet?) //populateDisplayedItemCounts(); - sortByDate(); + + // FIXME: Sorting by date should be done serverside (we should always get + // the most recent purchases on the 1st page) + //sortByDate(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index b41d04f718..76a963f63f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -31,10 +31,7 @@ Item { if (visible) { transactionHistoryModel.clear(); Commerce.balance(); - transactionHistoryModel.initialResultReceived = false; - transactionHistoryModel.currentPageToRetrieve = 1; - transactionHistoryModel.noMoreDataToRetrieve = false; - transactionHistoryModel.getPage(); + transactionHistoryModel.getFirstPage(); Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 41134ddfdb..f3b14fedd7 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -71,6 +71,15 @@ Item { console.log("Fetching Page " + root.currentPageToRetrieve + " of " + root.listModelName + "..."); } } + + // A helper function used to get the first page from the server. + // It can be custom-defined by this item's Parent. + property var getFirstPage: function() { + root.initialResultReceived = false; + root.currentPageToRetrieve = 1; + root.noMoreDataToRetrieve = false; + root.getPage(); + } // Resets both internal `ListModel`s and resets the page to retrieve to "1". function resetModel() { @@ -82,22 +91,22 @@ Item { onEndpointChanged: { resetModel(); - root.getPage(); + root.getFirstPage(); } onSortKeyChanged: { resetModel(); - root.getPage(); + root.getFirstPage(); } onSearchFilterChanged: { resetModel(); - root.getPage(); + root.getFirstPage(); } onTagsFilterChanged: { resetModel(); - root.getPage(); + root.getFirstPage(); } property bool initialResultReceived: false; From 235971f8bfd72b83cbefedb691d32b6028d571c5 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 24 May 2018 15:23:45 -0700 Subject: [PATCH 06/32] checkpoint --- .../common/sendAsset/ConnectionItem.qml | 2 +- .../commerce/common/sendAsset/SendAsset.qml | 31 +-- .../qml/hifi/commerce/wallet/Wallet.qml | 21 ++ .../qml/hifi/commerce/wallet/WalletHome.qml | 63 ++--- .../qml/hifi/models/PSFListModel.qml | 243 ++++++------------ scripts/system/commerce/wallet.js | 3 + scripts/system/marketplaces/marketplaces.js | 3 + 7 files changed, 144 insertions(+), 222 deletions(-) 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..403dde0713 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 "../../../models" as HifiModels Item { HifiConstants { id: hifi; } @@ -118,9 +119,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 +391,15 @@ Item { hoverEnabled: true; } - ListModel { + HifiModels.PSFListModel { id: connectionsModel; - } - ListModel { - id: filteredConnectionsModel; + http: root.parent; // Misuse of "root" in this file! + endpoint: "/api/v1/users?per_page=400&filter=connections"; // FIXME per_page + processPage: function (data) { + console.log("HRS FIXME processPage", JSON.stringify(data)); + return data.users; + //buildFilteredConnectionsModel(); + }; } Rectangle { @@ -495,6 +498,7 @@ Item { AnimatedImage { id: connectionsLoading; + visible: !connectionsModel.retrievedAtLeastOnePage; source: "../../../../../icons/profilePicLoading.gif" width: 120; height: width; @@ -515,14 +519,14 @@ Item { } visible: !connectionsLoading.visible; clip: true; - model: filteredConnectionsModel; + model: connectionsModel.model; 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; @@ -1806,13 +1810,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++) { diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 86700f702e..781420f2b2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -768,11 +768,32 @@ Rectangle { case 'updateSelectedRecipientUsername': sendMoney.fromScript(message); break; + case 'http.response': + handleHttpResponse(message); + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } } signal sendToScript(var message); + property var httpCalls: ({}); + property var httpCounter: 0; + function request(options, callback) { + console.debug('HRS FIXME Wallet request', JSON.stringify(options)); + httpCalls[httpCounter] = callback; + var message = {method: 'http.request', params: options, id: httpCounter++, jsonrpc: "2.0"}; + sendToScript(message); + } + 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.log('HRS FIXME QML handling of', JSON.stringify(message)); + callback(message.error, message.response); + } // generateUUID() taken from: // https://stackoverflow.com/a/8809472 diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 76a963f63f..efd51bfdf3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -24,12 +24,9 @@ Item { HifiConstants { id: hifi; } id: root; - property bool initialResultReceived: false; - property int pendingCount: 0; onVisibleChanged: { if (visible) { - transactionHistoryModel.clear(); Commerce.balance(); transactionHistoryModel.getFirstPage(); Commerce.getAvailableUpdates(); @@ -46,7 +43,7 @@ Item { } onHistoryResult : { - transactionHistoryModel.pageRetrieved(result); + transactionHistoryModel.handlePage(null, result); } onAvailableUpdatesResult: { @@ -61,7 +58,7 @@ Item { Connections { target: GlobalServices onMyUsernameChanged: { - transactionHistoryModel.clear(); + transactionHistoryModel.resetModel(); usernameText.text = Account.username; } } @@ -150,8 +147,7 @@ Item { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); Commerce.balance(); - transactionHistoryModel.currentPageToRetrieve = 1; - transactionHistoryModel.getPage(); + transactionHistoryModel.getFirstPage(); } } } @@ -218,22 +214,34 @@ Item { listModelName: "transaction history"; itemsPerPage: 100; - getPage: function() { - transactionHistoryModel.requestPending = true; + getPage: function () { + console.log('HRS FIXME WalletHome getPage', transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); } - pageRetrieved: function(result) { - transactionHistoryModel.processResult(result.status, result.data.history); - - if (!transactionHistoryModel.noMoreDataToRetrieve) { - calculatePendingAndInvalidated(); + processPage: function (data) { + console.log('HRS FIXME WalletHome processPage', JSON.stringify(data)); + var result, pending; + if (transactionHistoryModel.currentPageToRetrieve == 1) { + pending = {transaction_type: "pendingCount", count: 0}; + result = [pending]; + } else { + pending = transactionHistoryModel.get(0); + result = []; } + 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 && !transactionHistoryModel.noMoreDataToRetrieve) { + if (transactionHistory.atYBeginning && data.history.length && transactionHistoryModel.currentPageToRetrieve >= 0) { refreshTimer.start(); } + return result; } } Item { @@ -243,8 +251,8 @@ Item { anchors.left: parent.left; anchors.right: parent.right; - Item { - visible: transactionHistoryModel.count === 0 && transactionHistoryModel.initialResultReceived; + 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; @@ -316,10 +324,10 @@ Item { model: transactionHistoryModel.model; 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; @@ -328,7 +336,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; @@ -432,21 +440,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() // diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index f3b14fedd7..bf67d30232 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -18,102 +18,96 @@ import QtQuick 2.7 Item { - id: root; // Used when printing debug statements - property string listModelName: ""; + property string listModelName: endpoint; - // Holds the value of the page that'll be retrieved the next time `getPage()` is called - property int currentPageToRetrieve: 1; - - // If defined, the endpoint that `getPage()` will hit (as long as there isn't a custom `getPage()` - // that does something fancy) + // 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; - // If defined, the sort key used when calling the above endpoint. - // (as long as there isn't a custom `getPage()` that does something fancy) property string sortKey; - // If defined, the search filter used when calling the above endpoint. - // (as long as there isn't a custom `getPage()` that does something fancy) property string searchFilter; - // If defined, the tags filter used when calling the above endpoint. - // (as long as there isn't a custom `getPage()` that does something fancy) property string tagsFilter; - // The number of items that'll be retrieved per page when calling `getPage()` - // (as long as there isn't a custom `getPage()` that does something fancy) + onEndpointChanged: getFirstPage(); + onSortKeyChanged: getFirstPage(); + onSearchFilterChanged: getFirstPage(); + onTagsFilterChanged: getFirstPage(); property int itemsPerPage: 100; - // This function, by default, will retrieve data from the above-defined `endpoint` with the - // sort and filter data as set above. It can be custom-defined by this item's Parent. - property var getPage: function() { - // Put code here that calls the `endpoint` with the proper `sortKey`, `searchFilter`, and `tagsFilter`. - // Whatever code goes here should define the below `pageRetrieved()` as - // the callback for after the page is asynchronously retrieved. - - // The parent of this Item can also define custom `getPage()` and `pageRetrieved()` functions. - // See `WalletHome.qml` as an example of a file that does this. `WalletHome.qml` must use that method because - // it hits an endpoint that must be authenticated via the Wallet. - console.log("default getPage()"); - } - // This function, by default, will handle the data retrieved using `getPage()` above. - // It can be custom-defined by this item's Parent. - property var pageRetrieved: function() { - console.log("default pageRetrieved()"); - } - - // This function, by default, will get the _next_ page of data according to `getPage()` if there - // isn't a pending request and if there's more data to retrieve. - // It can be custom-defined by this item's Parent. - property var getNextPage: function() { - if (!root.requestPending && !root.noMoreDataToRetrieve) { - root.requestPending = true; - root.currentPageToRetrieve++; - root.getPage(); - console.log("Fetching Page " + root.currentPageToRetrieve + " of " + root.listModelName + "..."); - } - } - - // A helper function used to get the first page from the server. - // It can be custom-defined by this item's Parent. - property var getFirstPage: function() { - root.initialResultReceived = false; - root.currentPageToRetrieve = 1; - root.noMoreDataToRetrieve = false; - root.getPage(); - } - + // State. + property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. + property bool retrievedAtLeastOnePage: false; // Resets both internal `ListModel`s and resets the page to retrieve to "1". function resetModel() { - pagesAlreadyAdded = new Array(); tempModel.clear(); finalModel.clear(); - root.currentPageToRetrieve = 1; + currentPageToRetrieve = 1; + retrievedAtLeastOnePage = false } - onEndpointChanged: { + // Processing one page. + + // 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; } + + // Check consistency and call processPage. + function handlePage(error, response) { + console.log("HRS FIXME got", endpoint, error, JSON.stringify(response)); + function fail(message) { + console.warn("Warning", listModelName, JSON.stringify(message)); + current_page_to_retrieve = -1; + requestPending = 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); + } + finalModel.append(processPage(response.data || response)); // FIXME keep index steady, and apply any post sort/filter + retrievedAtLeastOnePage = true; + } + + // Override either http or getPage. + property var http: null; // 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 in", listModelName); } + var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; + // FIXME: handle sort and search parameters, and per_page and page parameters + console.log("HRS FIXME requesting", url); + 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 () { resetModel(); - root.getFirstPage(); + requestPending = true; + getPage(); } - - onSortKeyChanged: { - resetModel(); - root.getFirstPage(); - } - - onSearchFilterChanged: { - resetModel(); - root.getFirstPage(); - } - - onTagsFilterChanged: { - resetModel(); - root.getFirstPage(); - } - - property bool initialResultReceived: false; - property bool requestPending: false; - property bool noMoreDataToRetrieve: false; - property var pagesAlreadyAdded: new Array(); + 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) { thisPSFListModelId.getNextPage(); } + // ...} + property var getNextPage: function () { + if (requestPending || currentPageToRetrieve < 0) { + return; + } + console.log("HRS FIXME Fetching Page " + currentPageToRetrieve + " of " + listModelName + "..."); + currentPageToRetrieve++; + requestPending = true; + getPage(); + } + // Redefining members and methods so that the parent of this Item // can use PSFListModel as they would a regular ListModel property alias model: finalModel; @@ -123,6 +117,8 @@ Item { function remove(index) { return finalModel.remove(index); } function setProperty(index, prop, value) { return finalModel.setProperty(index, prop, value); } function move(from, to, n) { return finalModel.move(from, to, n); } + function insert(index, newElement) { finalModel.insert(index, newElement); } + function append(newElements) { finalModel.append(newElements); } // Used while processing page data and sorting ListModel { @@ -134,97 +130,6 @@ Item { id: finalModel; } - function processResult(status, retrievedResult) { - root.initialResultReceived = true; - root.requestPending = false; - - if (status === 'success') { - var currentPage = parseInt(retrievedResult.current_page); - - if (retrievedResult.length === 0) { - root.noMoreDataToRetrieve = true; - console.log("No more data to retrieve from backend endpoint.") - } - /* - See FIXME below... - - else if (root.currentPageToRetrieve === 1) { - var sameItemCount = 0; - - tempModel.clear(); - tempModel.append(retrievedResult); - - for (var i = 0; i < tempModel.count; i++) { - if (!finalModel.get(i)) { - sameItemCount = -1; - break; - } - // Gotta think of a generic way to determine if the data we just got is the same - // as the data that we already have in the model. - else if (tempModel.get(i).transaction_type === finalModel.get(i).transaction_type && - tempModel.get(i).text === finalModel.get(i).text) { - sameItemCount++; - } - } - - if (sameItemCount !== tempModel.count) { - finalModel.clear(); - for (var i = 0; i < tempModel.count; i++) { - finalModel.append(tempModel.get(i)); - } - } - } - */ - else { - // FIXME! Reconsider this logic, because it means that auto-refreshing the first page of results - // (like we do in WalletHome for Recent Activity) _won't_ catch brand new data elements! - // See the commented code above for how I did this for WalletHome specifically. - if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { - console.log("Page " + currentPage + " of paginated data has already been added to the list."); - } else { - // First, add the result to a temporary model - tempModel.clear(); - tempModel.append(retrievedResult); - - // 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 (finalModel.count !== 0) { - var currentIteratorPage; - // Search through the whole model 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 < finalModel.count; i++) { - currentIteratorPage = finalModel.get(i).resultIsFromPage; - - if (currentPage < currentIteratorPage) { - insertionIndex = i; - break; - } else if (i === finalModel.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. - // NOTE that this wouldn't be necessary if we did this step (or a similar step) on the server. - for (var i = 0; i < tempModel.count; i++) { - tempModel.setProperty(i, "resultIsFromPage", currentPage); - finalModel.insert(i + insertionIndex, tempModel.get(i)) - } - } - } - return true; - } else { - console.log("Failed to get page result for " + root.listModelName); - } - - return false; - } // Used when sorting model data on the CLIENT // Right now, there is no sorting done on the client for diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index aea752c565..ac25269e41 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -511,6 +511,9 @@ case 'wallet_availableUpdatesReceived': // NOP break; + case 'http.request': + // Handled elsewhere, don't log. + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index dc4d5aa844..ea8278a459 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -988,6 +988,9 @@ var selectionDisplay = null; // for gridTool.js to ignore sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, SEND_ASSET_PARTICLE_TIMER_UPDATE); } break; + case 'http.request': + // Handled elsewhere, don't log. + break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } From d30d84f1bdccc2ef8717443e2b7c0e72eb7e8c1a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 25 May 2018 15:30:27 -0700 Subject: [PATCH 07/32] checkpoint that has two ways to do filtering --- .../commerce/common/sendAsset/SendAsset.qml | 22 +++----- .../qml/hifi/models/PSFListModel.qml | 55 ++++++++++++++----- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 403dde0713..208cf2f49e 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -394,12 +394,16 @@ Item { HifiModels.PSFListModel { id: connectionsModel; http: root.parent; // Misuse of "root" in this file! - endpoint: "/api/v1/users?per_page=400&filter=connections"; // FIXME per_page + endpoint: "/api/v1/users?filter=connections"; + itemsPerPage: 8; processPage: function (data) { console.log("HRS FIXME processPage", JSON.stringify(data)); return data.users; - //buildFilteredConnectionsModel(); }; + searchFilter: filterBar.text; + searchItemTest: function (text, item) { + return item.username.toLowerCase().indexOf(text.toLowerCase()) !== -1; + }; //HRS FIXME remove when endpoint works. } Rectangle { @@ -475,10 +479,6 @@ Item { anchors.fill: parent; centerPlaceholderGlyph: hifi.glyphs.search; - onTextChanged: { - buildFilteredConnectionsModel(); - } - onAccepted: { focus = false; } @@ -520,6 +520,7 @@ Item { visible: !connectionsLoading.visible; clip: true; model: connectionsModel.model; + onAtYEndChanged: if (connectionsList.atYEnd) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; @@ -1810,15 +1811,6 @@ Item { // FUNCTION DEFINITIONS START // - 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/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index bf67d30232..ece229bea8 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -26,23 +26,43 @@ Item { // E.g., your getPage function could refer to this sortKey, etc. property string endpoint; property string sortKey; - property string searchFilter; + property string searchFilter: ""; property string tagsFilter; onEndpointChanged: getFirstPage(); onSortKeyChanged: getFirstPage(); - onSearchFilterChanged: getFirstPage(); + onSearchFilterChanged: { + if (searchItemTest) { + var filteredCopy = copyOfItems.filter(function (item) { + return searchItemTest(searchFilter, item); + }); + finalModel.clear(); + finalModel.append(filteredCopy); + /*for (var index = 0; index < finalModel.count; index++) { + if (!searchItemTest(searchFilter, finalModel.get(index))) { + finalModel.remove(index); + index--; // Don't skip over anything now that the indices have shifted. + } + }*/ + } else { // TODO: fancy timer against fast typing. + getFirstPage(); + } + } onTagsFilterChanged: getFirstPage(); property int itemsPerPage: 100; + // If the endpoint doesn't do search, tags, sort, these functions can be supplied to do it here. + property var searchItemTest: nil; + property var copyOfItems: []; + // State. property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. property bool retrievedAtLeastOnePage: false; // Resets both internal `ListModel`s and resets the page to retrieve to "1". function resetModel() { - tempModel.clear(); finalModel.clear(); currentPageToRetrieve = 1; - retrievedAtLeastOnePage = false + retrievedAtLeastOnePage = false; + copyOfItems = []; } // Processing one page. @@ -52,6 +72,7 @@ Item { // Check consistency and call processPage. function handlePage(error, response) { + var processed; console.log("HRS FIXME got", endpoint, error, JSON.stringify(response)); function fail(message) { console.warn("Warning", listModelName, JSON.stringify(message)); @@ -68,7 +89,16 @@ Item { if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property. return fail("Mismatched page, expected:" + currentPageToRetrieve); } - finalModel.append(processPage(response.data || response)); // FIXME keep index steady, and apply any post sort/filter + processed = processPage(response.data || response); + if (searchItemTest) { + copyOfItems = copyOfItems.concat(processed); + if (searchFilter) { + processed = processed.filter(function (item) { + return searchItemTest(searchFilter, item); + }); + } + } + finalModel.append(processed); // FIXME keep index steady, and apply any post sort/filter retrievedAtLeastOnePage = true; } @@ -77,7 +107,13 @@ Item { 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 in", listModelName); } var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; - // FIXME: handle sort and search parameters, and per_page and page parameters + var parameters = [ + // FIXME: handle sort, search, tag parameters + 'per_page=' + itemsPerPage, + 'page=' + currentPageToRetrieve + ]; + var parametersSeparator = /\?/.test(url) ? '&' : '?'; + url = url + parametersSeparator + parameters.join('&'); console.log("HRS FIXME requesting", url); http.request({uri: url}, handlePage); } @@ -120,17 +156,10 @@ Item { function insert(index, newElement) { finalModel.insert(index, newElement); } function append(newElements) { finalModel.append(newElements); } - // Used while processing page data and sorting - ListModel { - id: tempModel; - } - - // This is the model that the parent of this Item will actually see ListModel { id: finalModel; } - // Used when sorting model data on the CLIENT // Right now, there is no sorting done on the client for // any users of PSFListModel, but that could very easily change. From 17be460adfba228a7bc6aa2ff9d111133befe9b2 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 25 May 2018 16:37:06 -0700 Subject: [PATCH 08/32] checkpoint --- .../commerce/common/sendAsset/SendAsset.qml | 2 +- .../qml/hifi/commerce/wallet/WalletHome.qml | 2 +- .../qml/hifi/models/PSFListModel.qml | 33 ++++++------ scripts/system/request-service.js | 50 +++++++++++++++++++ 4 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 scripts/system/request-service.js diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 208cf2f49e..24753e7b6a 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -558,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"; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index efd51bfdf3..b23f6ec16c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -147,7 +147,7 @@ Item { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); Commerce.balance(); - transactionHistoryModel.getFirstPage(); + transactionHistoryModel.getFirstPage("delayedClear"); } } } diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index ece229bea8..4dc96857af 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -32,17 +32,9 @@ Item { onSortKeyChanged: getFirstPage(); onSearchFilterChanged: { if (searchItemTest) { - var filteredCopy = copyOfItems.filter(function (item) { - return searchItemTest(searchFilter, item); - }); + var filteredCopy = applySearchItemTest(copyOfItems); finalModel.clear(); finalModel.append(filteredCopy); - /*for (var index = 0; index < finalModel.count; index++) { - if (!searchItemTest(searchFilter, finalModel.get(index))) { - finalModel.remove(index); - index--; // Don't skip over anything now that the indices have shifted. - } - }*/ } else { // TODO: fancy timer against fast typing. getFirstPage(); } @@ -57,9 +49,11 @@ Item { // State. property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. property bool retrievedAtLeastOnePage: false; - // Resets both internal `ListModel`s and resets the page to retrieve to "1". + // 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() { - finalModel.clear(); + if (!delayedClear) { finalModel.clear(); } currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; copyOfItems = []; @@ -78,6 +72,7 @@ Item { console.warn("Warning", listModelName, JSON.stringify(message)); current_page_to_retrieve = -1; requestPending = false; + delayedClear = false; } if (error || (response.status !== 'success')) { return fail(error || response.status); @@ -93,14 +88,21 @@ Item { if (searchItemTest) { copyOfItems = copyOfItems.concat(processed); if (searchFilter) { - processed = processed.filter(function (item) { - return searchItemTest(searchFilter, item); - }); + processed = applySearchItemTest(processed); } } + if (delayedClear) { + finalModel.clear(); + delayedClear = false; + } finalModel.append(processed); // FIXME keep index steady, and apply any post sort/filter retrievedAtLeastOnePage = true; } + function applySearchItemTest(items) { + return items.filter(function (item) { + return searchItemTest(searchFilter, item); + }); + } // Override either http or getPage. property var http: null; // An Item that has a request function. @@ -120,7 +122,8 @@ Item { // Start the show by retrieving data according to `getPage()`. // It can be custom-defined by this item's Parent. - property var getFirstPage: function () { + property var getFirstPage: function (delayClear) { + delayedClear = !!delayClear; resetModel(); requestPending = true; getPage(); diff --git a/scripts/system/request-service.js b/scripts/system/request-service.js new file mode 100644 index 0000000000..84e80489fa --- /dev/null +++ b/scripts/system/request-service.js @@ -0,0 +1,50 @@ +"use strict"; +// +// request-service.js +// +// Created by Howard Stearns on May 22, 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 +// + +(function() { // BEGIN LOCAL_SCOPE + + // QML has its own XMLHttpRequest, but: + // - npm request is easier to use. + // - It is not easy to hack QML's XMLHttpRequest to use our MetaverseServer, and to supply the user's auth when contacting it. + // a. Our custom XMLHttpRequestClass object only works with QScriptEngine, not QML's javascript. + // b. We have hacked profiles that intercept requests to our MetavserseServer (providing the correct auth), but those + // only work in QML WebEngineView. Setting up communication between ordinary QML and a hiddent WebEngineView is + // tantamount to the following anyway, and would still have to duplicate the code from request.js. + + // So, this script does two things: + // 1. Allows any root .qml to signal sendToScript({id: aString, method: 'http.request', params: byNameOptions}) + // We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body}) + // on that root object. + // 2. If the uri used (computed from byNameOptions, see request.js) begins with '/', we will: + // a. Prepend Account.metaverseServerUR. + // b. Use the appropriate auth. + + var request = Script.require('request').request; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + function fromQml(message) { // messages are {id, method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'http.request': + request(message.params, function (error, response) { + console.log('HRS FIXME request-service got', JSON.stringify(error), JSON.stringify(response)); + tablet.sendToQml({ + id: message.id, + method: 'http.response', + error: error, // Alas, this isn't always a JSON-RPC conforming error object. + response: response, + jsonrpc: '2.0' + }); + }); + break; + } + } + tablet.fromQml.connect(fromQml); + Script.scriptEnding.connect(function () { tablet.fromQml.disconnect(fromQml); }); +}()); // END LOCAL_SCOPE From bdd38cef7ace61369711ddab842aff7db6949d6e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 30 May 2018 17:09:13 -0700 Subject: [PATCH 09/32] checkpoint --- .../resources/qml/hifi/RootHttpRequest.qml | 39 ++++++++++++++++++ .../commerce/common/sendAsset/SendAsset.qml | 5 ++- .../qml/hifi/commerce/purchases/Purchases.qml | 40 +++++++++++++++---- .../qml/hifi/commerce/wallet/Wallet.qml | 29 +++++--------- .../qml/hifi/commerce/wallet/WalletHome.qml | 13 +++--- .../qml/hifi/models/PSFListModel.qml | 27 ++++++++----- scripts/system/request-service.js | 30 +++++++------- 7 files changed, 124 insertions(+), 59 deletions(-) create mode 100644 interface/resources/qml/hifi/RootHttpRequest.qml 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/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 24753e7b6a..13fe748ec7 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -37,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 @@ -393,7 +395,8 @@ Item { HifiModels.PSFListModel { id: connectionsModel; - http: root.parent; // Misuse of "root" in this file! + http: root.http; + listModelName: root.listModelName; endpoint: "/api/v1/users?filter=connections"; itemsPerPage: 8; processPage: function (data) { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 93400349a2..795bb2306a 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -21,6 +21,7 @@ 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 @@ -81,7 +82,7 @@ Rectangle { } onInventoryResult: { - purchasesModel.pageRetrieved(result); + purchasesModel.handlePage(result.status !== "success" && result.message, result); } onAvailableUpdatesResult: { @@ -145,8 +146,14 @@ Rectangle { } } + HifiCommon.RootHttpRequest { + id: http; + } + HifiSendAsset.SendAsset { id: sendAsset; + http: http; + listModelName: "Gift Connections"; z: 998; visible: root.activeView === "giftAsset"; anchors.fill: parent; @@ -547,9 +554,10 @@ Rectangle { HifiModels.PSFListModel { id: purchasesModel; + itemsPerPage: 6; - itemsPerPage: 3; - getPage: function() { + getPage: function () { + console.log('HRS FIXME Purchases getPage', root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); Commerce.inventory( root.isShowingMyItems ? "proofs" : "purchased", filterBar.primaryFilter_filterName.toLowerCase(), @@ -557,10 +565,24 @@ Rectangle { purchasesModel.itemsPerPage ); } - pageRetrieved: function(result) { - purchasesReceived = true; + 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 = ''; + // HRS FIXME updateable + }); + // HRS FIXME purchaess_updateWearables + // HRS FIXME populateDisplayedItemCounts + // HRS FIXME sortByDate + return data.assets; - var processedInventory = processInventoryResult(result.data.assets); + /* + var processedInventory = processInventoryResult(data.assets); if (purchasesModel.processResult(result.status, processedInventory)) { var currentId; @@ -592,6 +614,7 @@ Rectangle { // the most recent purchases on the 1st page) //sortByDate(); } + */ } } @@ -970,7 +993,7 @@ Rectangle { // FUNCTION DEFINITIONS START // - function processInventoryResult(inventory) { + function processInventoryResult(inventory) { // HRS FIXME remove 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!") @@ -1087,6 +1110,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 781420f2b2..1e11cbc058 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; @@ -762,38 +769,22 @@ Rectangle { // NOP break; case 'updateConnections': + console.log('Wallet.qml updateConnections');// HRS FIXME sendMoney.updateConnections(message.connections); break; case 'selectRecipient': case 'updateSelectedRecipientUsername': + console.log('Wallet.qml updateSelectedRecipientUsername'); // HRS FIXME sendMoney.fromScript(message); break; case 'http.response': - handleHttpResponse(message); + http.handleHttpResponse(message); break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } } signal sendToScript(var message); - property var httpCalls: ({}); - property var httpCounter: 0; - function request(options, callback) { - console.debug('HRS FIXME Wallet request', JSON.stringify(options)); - httpCalls[httpCounter] = callback; - var message = {method: 'http.request', params: options, id: httpCounter++, jsonrpc: "2.0"}; - sendToScript(message); - } - 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.log('HRS FIXME QML handling of', JSON.stringify(message)); - callback(message.error, message.response); - } // generateUUID() taken from: // https://stackoverflow.com/a/8809472 diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index b23f6ec16c..6d41da1e6e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -211,16 +211,15 @@ Item { HifiModels.PSFListModel { id: transactionHistoryModel; - - listModelName: "transaction history"; - itemsPerPage: 100; + 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.log('HRS FIXME WalletHome getPage', transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); + console.debug('WalletHome getPage', transactionHistoryModel.currentPageToRetrieve); Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); } processPage: function (data) { - console.log('HRS FIXME WalletHome processPage', JSON.stringify(data)); - var result, pending; + console.debug('WalletHome processPage', 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]; @@ -228,6 +227,8 @@ Item { pending = transactionHistoryModel.get(0); result = []; } + + // Either add to pending, or to result. data.history.forEach(function (item) { if (item.status === 'pending') { pending.count++; diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 4dc96857af..daafec4e62 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -18,7 +18,7 @@ import QtQuick 2.7 Item { - + id: root; // Used when printing debug statements property string listModelName: endpoint; @@ -28,9 +28,14 @@ Item { property string sortKey; property string searchFilter: ""; property string tagsFilter; - onEndpointChanged: getFirstPage(); - onSortKeyChanged: getFirstPage(); + + // 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(); } + onSortKeyChanged: if (initialized) { getFirstPage(); } onSearchFilterChanged: { + if (!initialized) { return; } if (searchItemTest) { var filteredCopy = applySearchItemTest(copyOfItems); finalModel.clear(); @@ -39,11 +44,11 @@ Item { getFirstPage(); } } - onTagsFilterChanged: getFirstPage(); + onTagsFilterChanged: if (initialized) { getFirstPage(); } property int itemsPerPage: 100; // If the endpoint doesn't do search, tags, sort, these functions can be supplied to do it here. - property var searchItemTest: nil; + property var searchItemTest: null; property var copyOfItems: []; // State. @@ -67,10 +72,10 @@ Item { // Check consistency and call processPage. function handlePage(error, response) { var processed; - console.log("HRS FIXME got", endpoint, error, JSON.stringify(response)); + console.debug('handlePage', listModelName, error, JSON.stringify(response)); function fail(message) { console.warn("Warning", listModelName, JSON.stringify(message)); - current_page_to_retrieve = -1; + currentPageToRetrieve = -1; requestPending = false; delayedClear = false; } @@ -105,9 +110,9 @@ Item { } // Override either http or getPage. - property var http: null; // An Item that has a request function. + 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 in", listModelName); } + if (!http) { return console.warn("Neither http nor getPage was set for", listModelName); } var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; var parameters = [ // FIXME: handle sort, search, tag parameters @@ -116,7 +121,7 @@ Item { ]; var parametersSeparator = /\?/.test(url) ? '&' : '?'; url = url + parametersSeparator + parameters.join('&'); - console.log("HRS FIXME requesting", url); + console.debug('getPage', listModelName); http.request({uri: url}, handlePage); } @@ -141,8 +146,8 @@ Item { if (requestPending || currentPageToRetrieve < 0) { return; } - console.log("HRS FIXME Fetching Page " + currentPageToRetrieve + " of " + listModelName + "..."); currentPageToRetrieve++; + console.debug("getNextPage", listModelName, currentPageToRetrieve); requestPending = true; getPage(); } diff --git a/scripts/system/request-service.js b/scripts/system/request-service.js index 84e80489fa..3c3b9ccc04 100644 --- a/scripts/system/request-service.js +++ b/scripts/system/request-service.js @@ -22,7 +22,8 @@ // So, this script does two things: // 1. Allows any root .qml to signal sendToScript({id: aString, method: 'http.request', params: byNameOptions}) // We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body}) - // on that root object. + // on that root object. + // RootHttpRequest.qml does this. // 2. If the uri used (computed from byNameOptions, see request.js) begins with '/', we will: // a. Prepend Account.metaverseServerUR. // b. Use the appropriate auth. @@ -30,20 +31,19 @@ var request = Script.require('request').request; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); function fromQml(message) { // messages are {id, method, params}, like json-rpc. See also sendToQml. - switch (message.method) { - case 'http.request': - request(message.params, function (error, response) { - console.log('HRS FIXME request-service got', JSON.stringify(error), JSON.stringify(response)); - tablet.sendToQml({ - id: message.id, - method: 'http.response', - error: error, // Alas, this isn't always a JSON-RPC conforming error object. - response: response, - jsonrpc: '2.0' - }); - }); - break; - } + switch (message.method) { + case 'http.request': + request(message.params, function (error, response) { + tablet.sendToQml({ + id: message.id, + method: 'http.response', + error: error, // Alas, this isn't always a JSON-RPC conforming error object. + response: response, + jsonrpc: '2.0' + }); + }); + break; + } } tablet.fromQml.connect(fromQml); Script.scriptEnding.connect(function () { tablet.fromQml.disconnect(fromQml); }); From e8e12eef8f9d2597c4e57886f838fa610ffcd591 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 31 May 2018 14:42:52 -0700 Subject: [PATCH 10/32] checkpoint --- .../commerce/common/sendAsset/SendAsset.qml | 5 ++-- .../qml/hifi/commerce/wallet/Wallet.qml | 3 +++ .../qml/hifi/commerce/wallet/WalletHome.qml | 23 ++----------------- .../qml/hifi/models/PSFListModel.qml | 21 +++++++++++++++-- scripts/system/marketplaces/marketplaces.js | 2 ++ 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 13fe748ec7..716758a3fe 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -399,8 +399,9 @@ Item { listModelName: root.listModelName; endpoint: "/api/v1/users?filter=connections"; itemsPerPage: 8; + listView: connectionsList; processPage: function (data) { - console.log("HRS FIXME processPage", JSON.stringify(data)); + console.log("processPage", connectionsModel.listModelName, JSON.stringify(data)); return data.users; }; searchFilter: filterBar.text; @@ -523,7 +524,7 @@ Item { visible: !connectionsLoading.visible; clip: true; model: connectionsModel.model; - onAtYEndChanged: if (connectionsList.atYEnd) { connectionsModel.getNextPage(); } + onAtYEndChanged: if (connectionsList.atYEnd /*&& !connectionsList.atYBeginning*/) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 1e11cbc058..b75141f8dd 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -780,6 +780,9 @@ Rectangle { case 'http.response': http.handleHttpResponse(message); break; + case 'palIsStale': + case 'avatarDisconnected': // HRS FIXME. What are these about? + 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 6d41da1e6e..1c0debb12b 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -214,11 +214,11 @@ Item { 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('WalletHome getPage', transactionHistoryModel.currentPageToRetrieve); + console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve); Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); } processPage: function (data) { - console.debug('WalletHome processPage', JSON.stringify(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}; @@ -441,25 +441,6 @@ Item { return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm; } - // - // 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 index daafec4e62..d9e31cbfa1 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -40,6 +40,7 @@ Item { var filteredCopy = applySearchItemTest(copyOfItems); finalModel.clear(); finalModel.append(filteredCopy); + debugView('after searchFilterChanged'); } else { // TODO: fancy timer against fast typing. getFirstPage(); } @@ -69,6 +70,7 @@ Item { // 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; @@ -100,14 +102,28 @@ Item { finalModel.clear(); delayedClear = false; } - finalModel.append(processed); // FIXME keep index steady, and apply any post sort/filter + finalModel.append(processed); // FIXME keep index steady, and apply any post sort retrievedAtLeastOnePage = true; + if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + currentPageToRetrieve = -1; + } + debugView('after handlePage'); + if (searchItemTest && searchFilter && listView && listView.atYEnd && (currentPageToRetrieve >= 0)) { + getNextPage(); // too fancy?? + } } function applySearchItemTest(items) { return items.filter(function (item) { return searchItemTest(searchFilter, item); }); } + 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. @@ -121,7 +137,7 @@ Item { ]; var parametersSeparator = /\?/.test(url) ? '&' : '?'; url = url + parametersSeparator + parameters.join('&'); - console.debug('getPage', listModelName); + console.debug('getPage', listModelName, currentPageToRetrieve); http.request({uri: url}, handlePage); } @@ -131,6 +147,7 @@ Item { delayedClear = !!delayClear; resetModel(); requestPending = true; + console.debug("getFirstPage", listModelName, currentPageToRetrieve); getPage(); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index ea8278a459..208e64fd5e 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -991,6 +991,8 @@ var selectionDisplay = null; // for gridTool.js to ignore case 'http.request': // Handled elsewhere, don't log. break; + case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? + break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } From 29b09d64e68292595952b8d664e79f5f3049ba79 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 1 Jun 2018 13:33:45 -0700 Subject: [PATCH 11/32] checkpoint with PAL working. --- interface/resources/qml/hifi/Pal.qml | 113 +++++----------- .../commerce/common/sendAsset/SendAsset.qml | 1 - .../qml/hifi/models/PSFListModel.qml | 122 ++++++------------ scripts/system/pal.js | 2 + 4 files changed, 74 insertions(+), 164 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index d779b4ba42..8a067c0733 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 "models" as HifiModels // references HMD, Users, UserActivityLogger from root context @@ -44,6 +45,28 @@ Rectangle { property bool punctuationMode: false; HifiConstants { id: hifi; } + RootHttpRequest { id: http; } + HifiModels.PSFListModel { + id: connectionsUserModel; + http: http; + endpoint: "/api/v1/users?filter=connections"; + localSort: true; + property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + sortProperty: sortColumn ? sortColumn.role : "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 +129,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 +245,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 +269,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 @@ -702,7 +709,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 +754,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 +778,10 @@ Rectangle { resizable: false; } - model: ListModel { - id: connectionsUserModel; + model: connectionsUserModel.model; + Connections { + target: connectionsTable.flickableItem; + onAtYEndChanged: if (connectionsTable.flickableItem.atYEnd) { connectionsUserModel.getNextPage(); } } // This Rectangle refers to each Row in the connectionsTable. @@ -1130,16 +1131,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 +1230,11 @@ Rectangle { reloadNearby.color = 2; } break; + case 'inspectionCertificate_resetCert': // HRS FIXME what's this about? + break; + case 'http.response': + http.handleHttpResponse(message); + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } @@ -1287,45 +1283,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/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 716758a3fe..21d803b1ab 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -401,7 +401,6 @@ Item { itemsPerPage: 8; listView: connectionsList; processPage: function (data) { - console.log("processPage", connectionsModel.listModelName, JSON.stringify(data)); return data.users; }; searchFilter: filterBar.text; diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index d9e31cbfa1..9858d76d4a 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -25,15 +25,17 @@ Item { // 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 sortKey; + 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(); } - onSortKeyChanged: if (initialized) { getFirstPage(); } + onEndpointChanged: if (initialized) { getFirstPage('delayClear'); } + onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } onSearchFilterChanged: { if (!initialized) { return; } if (searchItemTest) { @@ -42,14 +44,15 @@ Item { finalModel.append(filteredCopy); debugView('after searchFilterChanged'); } else { // TODO: fancy timer against fast typing. - getFirstPage(); + getFirstPage('delayClear'); } } - onTagsFilterChanged: if (initialized) { getFirstPage(); } + onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); } property int itemsPerPage: 100; // If the endpoint doesn't do search, tags, sort, these functions can be supplied to do it here. property var searchItemTest: null; + property bool localSort: false; property var copyOfItems: []; // State. @@ -92,21 +95,29 @@ Item { return fail("Mismatched page, expected:" + currentPageToRetrieve); } processed = processPage(response.data || response); + if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + currentPageToRetrieve = -1; + } if (searchItemTest) { copyOfItems = copyOfItems.concat(processed); if (searchFilter) { processed = applySearchItemTest(processed); } } + if (localSort) { + copyOfItems = copyOfItems.concat(processed); + if (sortProperty) { + sortCopy(sortProperty, sortAscending); + processed = copyOfItems; + delayedClear = true; // see next conditional + } + } if (delayedClear) { finalModel.clear(); delayedClear = false; } finalModel.append(processed); // FIXME keep index steady, and apply any post sort retrievedAtLeastOnePage = true; - if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { - currentPageToRetrieve = -1; - } debugView('after handlePage'); if (searchItemTest && searchFilter && listView && listView.atYEnd && (currentPageToRetrieve >= 0)) { getNextPage(); // too fancy?? @@ -185,85 +196,26 @@ Item { id: finalModel; } - // Used when sorting model data on the CLIENT - // Right now, there is no sorting done on the client for - // any users of PSFListModel, but that could very easily change. - property string sortColumnName: ""; - property bool isSortingDescending: true; - property bool valuesAreNumerical: false; + function sortCopy(sortProperty, isAscending) { + console.debug('client sort', listModelName, sortProperty, isAscending, copyOfItems.length, 'items'); + var before = isAscending ? -1 : 1; + var after = -1 * before; - function swap(a, b) { - if (a < b) { - move(a, b, 1); - move(b - 1, a, 1); - } else if (a > b) { - move(b, a, 1); - move(a - 1, b, 1); - } - } - - function partition(begin, end, pivot) { - if (valuesAreNumerical) { - var piv = get(pivot)[sortColumnName]; - swap(pivot, end - 1); - var store = begin; - var i; - - for (i = begin; i < end - 1; ++i) { - var currentElement = get(i)[sortColumnName]; - if (isSortingDescending) { - if (currentElement > piv) { - swap(store, i); - ++store; - } - } else { - if (currentElement < piv) { - swap(store, i); - ++store; - } - } + copyOfItems.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; } - swap(end - 1, store); - - return store; - } else { - var piv = get(pivot)[sortColumnName].toLowerCase(); - swap(pivot, end - 1); - var store = begin; - var i; - - for (i = begin; i < end - 1; ++i) { - var currentElement = get(i)[sortColumnName].toLowerCase(); - if (isSortingDescending) { - if (currentElement > piv) { - swap(store, i); - ++store; - } - } else { - if (currentElement < piv) { - swap(store, i); - ++store; - } - } + switch (true) { + case (aValue < bValue): return before; + case (aValue > bValue): return after; + default: return 0; } - swap(end - 1, store); - - return store; - } - } - - function qsort(begin, end) { - if (end - 1 > begin) { - var pivot = begin + Math.floor(Math.random() * (end - begin)); - - pivot = partition(begin, end, pivot); - - qsort(begin, pivot); - qsort(pivot + 1, end); - } - } - - function quickSort() { - qsort(0, count) + }); } } \ No newline at end of file diff --git a/scripts/system/pal.js b/scripts/system/pal.js index c70c2729f5..b122a5170a 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -317,6 +317,8 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } ); break; + case 'http.request': + break; // Handled elsewhere. default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } From 254abfa04a6ed73c2a3fcc2ee39d2bd6c269b791 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 4 Jun 2018 12:59:08 -0700 Subject: [PATCH 12/32] add request-services to default scripts. --- scripts/defaultScripts.js | 1 + scripts/system/request-service.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 59a51830be..6ea9f4cb81 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,6 +12,7 @@ // var DEFAULT_SCRIPTS_COMBINED = [ + "system/request-service.js", "system/progress.js", "system/away.js", "system/audio.js", diff --git a/scripts/system/request-service.js b/scripts/system/request-service.js index 3c3b9ccc04..b57f2d4cd7 100644 --- a/scripts/system/request-service.js +++ b/scripts/system/request-service.js @@ -24,9 +24,7 @@ // We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body}) // on that root object. // RootHttpRequest.qml does this. - // 2. If the uri used (computed from byNameOptions, see request.js) begins with '/', we will: - // a. Prepend Account.metaverseServerUR. - // b. Use the appropriate auth. + // 2. If the uri used (computed from byNameOptions, see request.js) is to our metaverse, we will use the appropriate auth. var request = Script.require('request').request; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); From e40fc20cb67bf4fd3259b030b5c0b794d81655fc Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 4 Jun 2018 13:00:08 -0700 Subject: [PATCH 13/32] fix some bugs that had been introduced by various out-of-team prs. --- interface/resources/qml/controls-uit/TextField.qml | 6 +++--- interface/resources/qml/hifi/Pal.qml | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) 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/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8a067c0733..9a818ef4db 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -479,7 +479,6 @@ Rectangle { visible: !isCheckBox && !isButton && !isAvgAudio; uuid: model ? model.sessionId : ""; selected: styleData.selected; - isReplicated: model.isReplicated; isAdmin: model && model.admin; isPresent: model && model.isPresent; // Size From 5a1b56d5730094ea889748bfc4f42dfecb997620 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 4 Jun 2018 13:11:06 -0700 Subject: [PATCH 14/32] checkpoint with goto feeds working (but without filtering) --- interface/resources/qml/hifi/Feed.qml | 52 ++++++++++++++----- .../qml/hifi/models/PSFListModel.qml | 7 ++- .../qml/hifi/tablet/TabletAddressDialog.qml | 40 +++----------- 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 3f3a47a297..98721ba2e0 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 "models" as HifiModels Column { id: root; - visible: false; + visible: !!suggestions.count; property int cardWidth: 212; property int cardHeight: 152; @@ -32,18 +33,37 @@ Column { property int stackedCardShadowHeight: 4; property int labelSize: 20; - property string metaverseServerUrl: ''; + property string metaverseServerUrl: ''; // FIXME loose this? 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(); + // FIXME onFilterChanged: filterChoicesByText(); property var goFunction: null; - property var rpc: null; + property var http: null; HifiConstants { id: hifi } - ListModel { id: suggestions; } + //FIXME 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=' + Window.protocolSignature() + ]; + endpoint: '/api/v1/user_stories?' + options.join('&'); + itemsPerPage: 3; + processPage: function (data) { + console.log('FIXME processPage', suggestions.listModelName, JSON.stringify(data)); + return data.user_stories.map(makeModelData); + }; + listModelName: actions; + listView: scroll; + } function resolveUrl(url) { return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url; @@ -60,11 +80,11 @@ Column { data.details.connections = 4; data.action = 'announcement'; } - return { + var fixme = { 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), @@ -77,8 +97,11 @@ Column { drillDownToPlace: false, searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } + }; + console.log('fixme makeModelData', JSON.stringify(fixme)); + return fixme; } + /* FIXME property var allStories: []; property var placeMap: ({}); // Used for making stacks. property int requestId: 0; @@ -108,7 +131,7 @@ Column { ]; var url = metaverseBase + 'user_stories?' + options.join('&'); var thisRequestId = ++requestId; - rpc('request', url, function (error, data) { + http.request(url, function (error, data) { if (thisRequestId !== requestId) { error = 'stale'; } @@ -126,8 +149,9 @@ Column { }); } function fillDestinations() { // Public - console.debug('Feed::fillDestinations()') - + console.debug('Feed::fillDestinations()'); + //suggestions.getFirstPage(); + } function report(label, error) { console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); } @@ -193,6 +217,7 @@ Column { allStories.forEach(makeFilteredStoryProcessor()); root.visible = !!suggestions.count; } + */ RalewayBold { id: label; @@ -202,12 +227,13 @@ Column { } ListView { id: scroll; - model: suggestions; + model: suggestions.model; orientation: ListView.Horizontal; highlightFollowsCurrentItem: false highlightMoveDuration: -1; highlightMoveVelocity: -1; currentIndex: -1; + onAtXEndChanged: { console.log('FIXME onAtXEndChanged', actions, scroll.atXEnd, scroll.atXBeginning); if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } spacing: 12; width: parent.width; @@ -239,6 +265,7 @@ Column { unhoverThunk: function () { hovered = false } } } + /* WTF is this? NumberAnimation { id: anim; target: scroll; @@ -256,4 +283,5 @@ Column { scroll.currentIndex = index; anim.running = true; } + */ } diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 9858d76d4a..6eff1cc073 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -63,7 +63,7 @@ Item { property bool delayedClear: false; function resetModel() { if (!delayedClear) { finalModel.clear(); } - currentPageToRetrieve = 1; + currentPageToRetrieve = 1; console.log('fixme resetModel set currentPageToRetrieve to 1', listModelName); retrievedAtLeastOnePage = false; copyOfItems = []; } @@ -80,6 +80,7 @@ Item { console.debug('handlePage', listModelName, error, JSON.stringify(response)); function fail(message) { console.warn("Warning", listModelName, JSON.stringify(message)); + console.log('FIXME fail setting currentPageToRetrieve to -1', listModelName); currentPageToRetrieve = -1; requestPending = false; delayedClear = false; @@ -96,6 +97,7 @@ Item { } processed = processPage(response.data || response); if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + console.log('fixme hanglePage set currentPageToRetrieve to -1', listModelName, 'response.total_pages:', response.total_pages, 'old currentPageToRetrieve:', currentPageToRetrieve); currentPageToRetrieve = -1; } if (searchItemTest) { @@ -122,6 +124,7 @@ Item { if (searchItemTest && searchFilter && listView && listView.atYEnd && (currentPageToRetrieve >= 0)) { getNextPage(); // too fancy?? } + if (listView) { console.debug('handlePage completed', listModelName, 'model:', model.count, 'view:', listView.count); } } function applySearchItemTest(items) { return items.filter(function (item) { @@ -140,6 +143,7 @@ Item { 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 = [ // FIXME: handle sort, search, tag parameters @@ -171,6 +175,7 @@ Item { // onAtYEndChanged: if (theList.atYEnd) { thisPSFListModelId.getNextPage(); } // ...} property var getNextPage: function () { + console.log('fixme getNextPage', listModelName, requestPending, currentPageToRetrieve); if (requestPending || currentPageToRetrieve < 0) { return; } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index dc67494e27..104756967e 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -37,38 +37,14 @@ StackView { 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 {} } @@ -351,7 +327,7 @@ StackView { actions: 'announcement'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: places; @@ -364,7 +340,7 @@ StackView { actions: 'concurrency'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: snapshots; @@ -378,7 +354,7 @@ StackView { actions: 'snapshot'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } } } From 5257a254cb95498d290ff34b41854ff022043b46 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 4 Jun 2018 13:12:41 -0700 Subject: [PATCH 15/32] more for the previous --- scripts/system/pal.js | 2 +- scripts/system/tablet-goto.js | 37 ----------------------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/scripts/system/pal.js b/scripts/system/pal.js index b122a5170a..41774629e7 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -318,7 +318,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See ); break; case 'http.request': - break; // Handled elsewhere. + break; // Handled by request-service. default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 9cd8420a88..46ddeb2bab 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -41,24 +41,6 @@ sortOrder: 8 }); - function fromQml(message) { - console.debug('tablet-goto::fromQml: message = ', JSON.stringify(message)); - - var response = {id: message.id, jsonrpc: "2.0"}; - switch (message.method) { - case 'request': - request(message.params, function (error, data) { - debug('rpc', request, 'error:', error, 'data:', data); - response.error = error; - response.result = data; - tablet.sendToQml(response); - }); - return; - default: - response.error = {message: 'Unrecognized message', data: message}; - } - tablet.sendToQml(response); - } function messagesWaiting(isWaiting) { button.editProperties({ icon: isWaiting ? WAITING_ICON : NORMAL_ICON @@ -66,21 +48,6 @@ }); } - var hasEventBridge = false; - function wireEventBridge(on) { - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } - } - } - function onClicked() { if (onGotoScreen) { // for toolbar-mode: go back to home screen, this will close the window. @@ -98,15 +65,11 @@ onGotoScreen = true; shouldActivateButton = true; button.editProperties({isActive: shouldActivateButton}); - wireEventBridge(true); messagesWaiting(false); - tablet.sendToQml({ method: 'refreshFeeds', protocolSignature: Window.protocolSignature() }) - } else { shouldActivateButton = false; onGotoScreen = false; button.editProperties({isActive: shouldActivateButton}); - wireEventBridge(false); } } button.clicked.connect(onClicked); From f72f8c762b3731b137f21784d6e9e5dd6300e7fd Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 4 Jun 2018 16:52:20 -0700 Subject: [PATCH 16/32] goto client-side filtering (preliminary) --- interface/resources/qml/hifi/Feed.qml | 15 ++++++++++----- .../resources/qml/hifi/models/PSFListModel.qml | 2 +- .../qml/hifi/tablet/TabletAddressDialog.qml | 4 ---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 98721ba2e0..fcfb61b1ca 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -33,7 +33,6 @@ Column { property int stackedCardShadowHeight: 4; property int labelSize: 20; - property string metaverseServerUrl: ''; // FIXME loose this? 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. @@ -63,10 +62,16 @@ Column { }; listModelName: actions; listView: scroll; + searchFilter: filter.toUpperCase().split(/\s+/).filter(identity).join(' '); + searchItemTest: function (text, item) { + return searchFilter.split().every(function (word) { + return item.searchText.indexOf(word) >= 0; + }); + }; //HRS FIXME remove when endpoint works. } 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. @@ -101,6 +106,9 @@ Column { console.log('fixme makeModelData', JSON.stringify(fixme)); return fixme; } + function identity(x) { + return x; + } /* FIXME property var allStories: []; property var placeMap: ({}); // Used for making stacks. @@ -172,9 +180,6 @@ Column { 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) { diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 6eff1cc073..124e08b6cd 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -101,10 +101,10 @@ Item { currentPageToRetrieve = -1; } if (searchItemTest) { - copyOfItems = copyOfItems.concat(processed); if (searchFilter) { processed = applySearchItemTest(processed); } + copyOfItems = copyOfItems.concat(processed); } if (localSort) { copyOfItems = copyOfItems.concat(processed); diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 104756967e..08f86770e6 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -34,7 +34,6 @@ 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; RootHttpRequest { id: http; } @@ -322,7 +321,6 @@ StackView { width: parent.width; cardWidth: 312 + (2 * 4); cardHeight: 163 + (2 * 4); - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'HAPPENING NOW'; actions: 'announcement'; filter: addressLine.text; @@ -335,7 +333,6 @@ StackView { cardWidth: 210; cardHeight: 110 + messageHeight; messageHeight: 44; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'PLACES'; actions: 'concurrency'; filter: addressLine.text; @@ -349,7 +346,6 @@ StackView { cardHeight: 75 + messageHeight + 4; messageHeight: 32; textPadding: 6; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'RECENT SNAPS'; actions: 'snapshot'; filter: addressLine.text; From 6aa551b35c64ea3ace2a39f93af4321e1d2d9b6e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 5 Jun 2018 17:04:29 -0700 Subject: [PATCH 17/32] Don't getNextPage when at beginning (even if that is also the end). --- .../resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml | 2 +- interface/resources/qml/hifi/commerce/wallet/WalletHome.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 21d803b1ab..a416030711 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -523,7 +523,7 @@ Item { visible: !connectionsLoading.visible; clip: true; model: connectionsModel.model; - onAtYEndChanged: if (connectionsList.atYEnd /*&& !connectionsList.atYBeginning*/) { connectionsModel.getNextPage(); } + onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 1c0debb12b..047dcd70d1 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -239,7 +239,7 @@ Item { // Only auto-refresh if the user hasn't scrolled // and there is more data to grab - if (transactionHistory.atYBeginning && data.history.length && transactionHistoryModel.currentPageToRetrieve >= 0) { + if (transactionHistory.atYBeginning && data.history.length) { refreshTimer.start(); } return result; From 335aeaeb3842c9a4d3b71ea0a0b37396fda0d6ea Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 5 Jun 2018 17:06:08 -0700 Subject: [PATCH 18/32] purchases cleanup, wearables, and edition numbers when multiples seen. --- .../qml/hifi/commerce/purchases/Purchases.qml | 79 ++++++------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 795bb2306a..3ef4d84754 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -39,8 +39,16 @@ Rectangle { property string installedApps; property bool keyboardRaised: false; property int numUpdatesAvailable: 0; + property var itemCountDictionary: ({}); // Style color: hifi.colors.white; + function getPurchases() { + root.activeView = "purchasesMain"; + root.itemCountDictionary = {}; + root.installedApps = Commerce.getInstalledApps(); + purchasesModel.getFirstPage(); + Commerce.getAvailableUpdates(); + } Connections { target: Commerce; @@ -63,10 +71,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(); - purchasesModel.getFirstPage(); - Commerce.getAvailableUpdates(); + getPurchases(); } } else { console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); @@ -104,8 +109,7 @@ Rectangle { } onIsShowingMyItemsChanged: { - purchasesModel.resetModel(); - + getPurchases(); } Timer { @@ -163,9 +167,7 @@ Rectangle { Connections { onSendSignalToParent: { if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') { - root.activeView = "purchasesMain"; - purchasesModel.getFirstPage(); - Commerce.getAvailableUpdates(); + getPurchases(); } else { sendToScript(msg); } @@ -429,10 +431,7 @@ Rectangle { case 'tutorial_skipClicked': case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); - root.activeView = "purchasesMain"; - root.installedApps = Commerce.getInstalledApps(); - purchasesModel.getFirstPage(); - Commerce.getAvailableUpdates(); + getPurchases(); break; } } @@ -508,7 +507,7 @@ Rectangle { }, { "displayName": "Content Set", - "filterName": "contentSet" + "filterName": "content_set" }, { "displayName": "Entity", @@ -555,9 +554,9 @@ Rectangle { HifiModels.PSFListModel { id: purchasesModel; itemsPerPage: 6; - + listModelName: 'purchases'; getPage: function () { - console.log('HRS FIXME Purchases getPage', root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); + console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); Commerce.inventory( root.isShowingMyItems ? "proofs" : "purchased", filterBar.primaryFilter_filterName.toLowerCase(), @@ -574,11 +573,14 @@ Rectangle { item.cardBackVisible = false; item.isInstalled = root.installedApps.indexOf(item.id) > -1; item.wornEntityID = ''; + item.displayedItemCount = itemCountDictionary[item.id] = (itemCountDictionary[item.id] || 0) + 1; // HRS FIXME updateable }); - // HRS FIXME purchaess_updateWearables - // HRS FIXME populateDisplayedItemCounts - // HRS FIXME sortByDate + sendToScript({ method: 'purchases_updateWearables' }); + for (var i = 0; i < purchasesModel.count; i++) { // Update all the previous counts with possibly new values. + purchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[purchasesModel.get(i).id]) + } + return data.assets; /* @@ -604,15 +606,6 @@ Rectangle { } } } - - sendToScript({ method: 'purchases_updateWearables' }); - // FIXME: This ALSO *MUST* be serverside (what if we don't have - // all instances of the item on the client yet?) - //populateDisplayedItemCounts(); - - // FIXME: Sorting by date should be done serverside (we should always get - // the most recent purchases on the 1st page) - //sortByDate(); } */ } @@ -807,7 +800,7 @@ Rectangle { onAtYEndChanged: { - if (purchasesContentsList.atYEnd) { + if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) { console.log("User scrolled to the bottom of 'Purchases'."); purchasesModel.getNextPage(); } @@ -992,7 +985,7 @@ Rectangle { // // FUNCTION DEFINITIONS START // - + /* fixme remove function processInventoryResult(inventory) { // HRS FIXME remove for (var i = 0; i < inventory.length; i++) { if (inventory[i].status.length > 1) { @@ -1002,31 +995,7 @@ Rectangle { inventory[i].categories = inventory[i].categories.join(';'); } return inventory; - } - - function populateDisplayedItemCounts() { - var itemCountDictionary = {}; - var currentItemId; - for (var i = 0; i < purchasesModel.count; i++) { - currentItemId = purchasesModel.get(i).id; - if (itemCountDictionary[currentItemId] === undefined) { - itemCountDictionary[currentItemId] = 1; - } else { - itemCountDictionary[currentItemId]++; - } - } - - for (var i = 0; i < purchasesModel.count; i++) { - purchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[purchasesModel.get(i).id]); - } - } - - function sortByDate() { - purchasesModel.sortColumnName = "purchase_date"; - purchasesModel.isSortingDescending = true; - purchasesModel.valuesAreNumerical = true; - purchasesModel.quickSort(); - } + } */ function updateCurrentlyWornWearables(wearables) { for (var i = 0; i < purchasesModel.count; i++) { From 8185d1b5f5d604f2013885d937ccc36e9f18a1f3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 6 Jun 2018 15:24:54 -0700 Subject: [PATCH 19/32] cleanup and consistently don't getNextPage at beginning --- interface/resources/qml/hifi/Feed.qml | 8 ++------ interface/resources/qml/hifi/Pal.qml | 6 +++++- .../resources/qml/hifi/commerce/wallet/WalletHome.qml | 2 +- interface/resources/qml/hifi/models/PSFListModel.qml | 6 ++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index fcfb61b1ca..1d28f18f9d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -43,7 +43,6 @@ Column { property var http: null; HifiConstants { id: hifi } - //FIXME ListModel { id: suggestions; } Component.onCompleted: suggestions.getFirstPage(); HifiModels.PSFListModel { id: suggestions; @@ -57,7 +56,6 @@ Column { endpoint: '/api/v1/user_stories?' + options.join('&'); itemsPerPage: 3; processPage: function (data) { - console.log('FIXME processPage', suggestions.listModelName, JSON.stringify(data)); return data.user_stories.map(makeModelData); }; listModelName: actions; @@ -85,7 +83,7 @@ Column { data.details.connections = 4; data.action = 'announcement'; } - var fixme = { + return { place_name: name, username: data.username || "", path: data.path || "", @@ -103,8 +101,6 @@ Column { searchText: [name].concat(tags, description || []).join(' ').toUpperCase() }; - console.log('fixme makeModelData', JSON.stringify(fixme)); - return fixme; } function identity(x) { return x; @@ -238,7 +234,7 @@ Column { highlightMoveDuration: -1; highlightMoveVelocity: -1; currentIndex: -1; - onAtXEndChanged: { console.log('FIXME onAtXEndChanged', actions, scroll.atXEnd, scroll.atXBeginning); if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } + onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } spacing: 12; width: parent.width; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 9a818ef4db..fbe34b2ebc 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -780,7 +780,11 @@ Rectangle { model: connectionsUserModel.model; Connections { target: connectionsTable.flickableItem; - onAtYEndChanged: if (connectionsTable.flickableItem.atYEnd) { connectionsUserModel.getNextPage(); } + onAtYEndChanged: { + if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) { + connectionsUserModel.getNextPage(); + } + } } // This Rectangle refers to each Row in the connectionsTable. diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 047dcd70d1..9076f10ebc 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -400,7 +400,7 @@ Item { } } onAtYEndChanged: { - if (transactionHistory.atYEnd) { + if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) { console.log("User scrolled to the bottom of 'Recent Activity'."); transactionHistoryModel.getNextPage(); } diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 124e08b6cd..5cca73af92 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -79,8 +79,7 @@ Item { var processed; console.debug('handlePage', listModelName, error, JSON.stringify(response)); function fail(message) { - console.warn("Warning", listModelName, JSON.stringify(message)); - console.log('FIXME fail setting currentPageToRetrieve to -1', listModelName); + console.warn("Warning page fail", listModelName, JSON.stringify(message)); currentPageToRetrieve = -1; requestPending = false; delayedClear = false; @@ -97,7 +96,6 @@ Item { } processed = processPage(response.data || response); if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { - console.log('fixme hanglePage set currentPageToRetrieve to -1', listModelName, 'response.total_pages:', response.total_pages, 'old currentPageToRetrieve:', currentPageToRetrieve); currentPageToRetrieve = -1; } if (searchItemTest) { @@ -172,7 +170,7 @@ Item { // ListView { // id: theList // model: thisPSFListModelId - // onAtYEndChanged: if (theList.atYEnd) { thisPSFListModelId.getNextPage(); } + // onAtYEndChanged: if (theList.atYEnd && !theList.atYBeginning) { thisPSFListModelId.getNextPage(); } // ...} property var getNextPage: function () { console.log('fixme getNextPage', listModelName, requestPending, currentPageToRetrieve); From 8b085051161890cde2e30b611e783d365d6ade2e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 7 Jun 2018 16:14:18 -0700 Subject: [PATCH 20/32] using filter/sort endpoint parameters --- interface/resources/qml/hifi/Feed.qml | 4 ++-- interface/resources/qml/hifi/Pal.qml | 13 +++++++++++-- .../hifi/commerce/common/sendAsset/SendAsset.qml | 4 ++-- .../resources/qml/hifi/models/PSFListModel.qml | 10 ++++++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 1d28f18f9d..4aa07f5d99 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -61,11 +61,11 @@ Column { listModelName: actions; listView: scroll; searchFilter: filter.toUpperCase().split(/\s+/).filter(identity).join(' '); - searchItemTest: function (text, item) { + /* FIXME searchItemTest: function (text, item) { return searchFilter.split().every(function (word) { return item.searchText.indexOf(word) >= 0; }); - }; //HRS FIXME remove when endpoint works. + };*/ //HRS FIXME remove when endpoint works. } function resolveUrl(url) { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index fbe34b2ebc..a1cc156fdd 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -50,9 +50,18 @@ Rectangle { id: connectionsUserModel; http: http; endpoint: "/api/v1/users?filter=connections"; - localSort: true; + //FIXME localSort: true; property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); - sortProperty: sortColumn ? sortColumn.role : "userName"; + 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; diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index a416030711..b3fd661088 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -404,9 +404,9 @@ Item { return data.users; }; searchFilter: filterBar.text; - searchItemTest: function (text, item) { + /* FIXME searchItemTest: function (text, item) { return item.username.toLowerCase().indexOf(text.toLowerCase()) !== -1; - }; //HRS FIXME remove when endpoint works. + };*/ //HRS FIXME remove when endpoint works. } Rectangle { diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 5cca73af92..b00d3684cf 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -27,7 +27,7 @@ Item { 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 sortKey: !sortProperty ? '' : (sortProperty + "," + (sortAscending ? "ASC" : "DESC")); property string searchFilter: ""; property string tagsFilter; @@ -144,10 +144,16 @@ Item { // If it is a path starting with slash, add the metaverseServer domain. var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; var parameters = [ - // FIXME: handle sort, search, tag parameters + // FIXME: handle sort, tag parameters 'per_page=' + itemsPerPage, 'page=' + currentPageToRetrieve ]; + if (!searchItemTest && searchFilter) { + parameters.splice(parameters.length, 0, 'search=' + searchFilter); + } + if (!localSort && sortKey) { + parameters.splice(parameters.length, 0, 'sort=' + sortKey); + } var parametersSeparator = /\?/.test(url) ? '&' : '?'; url = url + parametersSeparator + parameters.join('&'); console.debug('getPage', listModelName, currentPageToRetrieve); From 348d5fc359cfdc3b44a400004c45dd0d0e986ecc Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 7 Jun 2018 17:01:48 -0700 Subject: [PATCH 21/32] inventory text filter --- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 1 + interface/src/commerce/Ledger.cpp | 3 ++- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 4 ++-- interface/src/commerce/QmlCommerce.h | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 3ef4d84754..f7982717e7 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -560,6 +560,7 @@ Rectangle { Commerce.inventory( root.isShowingMyItems ? "proofs" : "purchased", filterBar.primaryFilter_filterName.toLowerCase(), + filterBar.text, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage ); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 5bd8c77d04..69698e82a6 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -134,10 +134,11 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage) { +void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { QJsonObject params; params["edition_filter"] = editionFilter; params["type_filter"] = typeFilter; + params["title_filter"] = titleFilter; params["page"] = page; params["per_page"] = perPage; keysQuery("inventory", "inventorySuccess", "inventoryFailure", params); diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 9733658357..8a8fd2630a 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -28,7 +28,7 @@ public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); - void inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage); + void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index dba8cd03c7..834de2150d 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -105,12 +105,12 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory(const QString& editionFilter, const QString& typeFilter, const int& page, const int& perPage) { +void QmlCommerce::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(editionFilter, typeFilter, page, perPage); + ledger->inventory(editionFilter, typeFilter, titleFilter, page, perPage); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 3a08b4a19b..a0c6916799 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -73,7 +73,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const int& page = 1, const int& perPage = 20); + Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); From 595a0d873367f26b1750e9d95e3d1f0f1c60f641 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 7 Jun 2018 19:24:05 -0700 Subject: [PATCH 22/32] restore lost code --- interface/resources/qml/hifi/Pal.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index a1cc156fdd..bac7533c9f 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -488,6 +488,7 @@ Rectangle { visible: !isCheckBox && !isButton && !isAvgAudio; uuid: model ? model.sessionId : ""; selected: styleData.selected; + isReplicated: model && model.isReplicated; isAdmin: model && model.admin; isPresent: model && model.isPresent; // Size From 64e466ee7dc6db85c4068bb5844fd5d37d11102d Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 7 Jun 2018 21:10:40 -0700 Subject: [PATCH 23/32] snapshot stacks --- interface/resources/qml/hifi/Feed.qml | 23 +++++++++++-------- .../qml/hifi/commerce/purchases/Purchases.qml | 2 +- .../qml/hifi/models/PSFListModel.qml | 3 ++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 4aa07f5d99..cd50eb14d8 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -41,6 +41,7 @@ Column { // FIXME onFilterChanged: filterChoicesByText(); property var goFunction: null; property var http: null; + property var itemCountDictionary: ({}); HifiConstants { id: hifi } Component.onCompleted: suggestions.getFirstPage(); @@ -55,12 +56,20 @@ Column { ]; endpoint: '/api/v1/user_stories?' + options.join('&'); itemsPerPage: 3; + getFirstPage: function (delayRefresh) { + root.itemCountDictionary = {}; + suggestions.getFirstPageInternal(delayRefresh); + }; processPage: function (data) { - return data.user_stories.map(makeModelData); + var adding = data.user_stories.map(makeModelData); + for (var i = 0; i < suggestions.count; i++) { // Update all the previous counts with possibly new values. + suggestions.setProperty(i, "drillDownToPlace", itemCountDictionary[suggestions.get(i).place_name] > 0); + } + return adding; }; listModelName: actions; listView: scroll; - searchFilter: filter.toUpperCase().split(/\s+/).filter(identity).join(' '); + searchFilter: filter; // FIXME .toUpperCase().split(/\s+/).filter(identity).join(' '); /* FIXME searchItemTest: function (text, item) { return searchFilter.split().every(function (word) { return item.searchText.indexOf(word) >= 0; @@ -78,11 +87,7 @@ 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'; - } + itemCountDictionary[name] = (itemCountDictionary[name] || 0) + 1; return { place_name: name, username: data.username || "", @@ -97,9 +102,9 @@ Column { tags: tags, description: description, online_users: data.details.connections || data.details.concurrency || 0, - drillDownToPlace: false, + drillDownToPlace: false - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() + //searchText: [name].concat(tags, description || []).join(' ').toUpperCase() // FIXME remove }; } function identity(x) { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index f7982717e7..accdc9984b 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -42,7 +42,7 @@ Rectangle { property var itemCountDictionary: ({}); // Style color: hifi.colors.white; - function getPurchases() { + function getPurchases() { // FIXME: use the new purchasesModel.getFirstPage root.activeView = "purchasesMain"; root.itemCountDictionary = {}; root.installedApps = Commerce.getInstalledApps(); diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index b00d3684cf..d17689ab98 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -162,7 +162,8 @@ Item { // Start the show by retrieving data according to `getPage()`. // It can be custom-defined by this item's Parent. - property var getFirstPage: function (delayClear) { + property var getFirstPage: function (delayClear) { getFirstPageInternal(delayClear); } + function getFirstPageInternal(delayClear) { delayedClear = !!delayClear; resetModel(); requestPending = true; From 21ed081eae4ae6b5e7c0f999c2eb616965ba655c Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 09:52:11 -0700 Subject: [PATCH 24/32] proper name for updated type filter of inventory endpoint --- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index accdc9984b..41ad978e33 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -519,7 +519,7 @@ Rectangle { }, { "displayName": "Updatable", - "filterName": "updatable" + "filterName": "updated" } ] filterBar.primaryFilterChoices.clear(); From 9f0864944cf48443dadde129cb7edd0c13c42579 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 8 Jun 2018 11:37:02 -0700 Subject: [PATCH 25/32] Fix Friending in Connections tab - READ FIXME --- interface/resources/qml/hifi/Pal.qml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index bac7533c9f..e457831a88 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -38,7 +38,6 @@ 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 @@ -873,12 +872,17 @@ 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}); + // HRS FIXME: NOTE FROM ZACH: With the line below uncommented, clicking any "FRIEND" checkbox + // in the table will result in the 0th table index FRIEND checkbox to become CHECKED. + // This is because there IS NO "model.userIndex" defined per entry in the connectionsUserModel. + // You could do one of two things here: + // 1. Programatically add a "userIndex" to each entry in the model as you fill it in (then this would work) + // 2. Not care about the model being accurate until its next refresh (at which point the "connection" + // property value will be correct, since the server will give the model the correct value) + //connectionsUserModel.setProperty(model.userIndex, styleData.role, (checked ? "friend" : "connection")); + 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); } } } From 3ade31bdf603600ea567c5e973959a3c3f501927 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 12:57:40 -0700 Subject: [PATCH 26/32] always show edition --- interface/resources/qml/hifi/Pal.qml | 8 -------- .../resources/qml/hifi/commerce/purchases/Purchases.qml | 9 +-------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index e457831a88..f5c47e5042 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -872,14 +872,6 @@ Rectangle { checked: model && (model.connection === "friend"); boxSize: 24; onClicked: { - // HRS FIXME: NOTE FROM ZACH: With the line below uncommented, clicking any "FRIEND" checkbox - // in the table will result in the 0th table index FRIEND checkbox to become CHECKED. - // This is because there IS NO "model.userIndex" defined per entry in the connectionsUserModel. - // You could do one of two things here: - // 1. Programatically add a "userIndex" to each entry in the model as you fill it in (then this would work) - // 2. Not care about the model being accurate until its next refresh (at which point the "connection" - // property value will be correct, since the server will give the model the correct value) - //connectionsUserModel.setProperty(model.userIndex, styleData.role, (checked ? "friend" : "connection")); pal.sendToScript({method: checked ? 'addFriend' : 'removeFriend', params: model.userName}); UserActivityLogger["palAction"](checked ? styleData.role : "un-" + styleData.role, model.sessionId); diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 41ad978e33..886dbbf5d8 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -39,12 +39,10 @@ Rectangle { property string installedApps; property bool keyboardRaised: false; property int numUpdatesAvailable: 0; - property var itemCountDictionary: ({}); // Style color: hifi.colors.white; function getPurchases() { // FIXME: use the new purchasesModel.getFirstPage root.activeView = "purchasesMain"; - root.itemCountDictionary = {}; root.installedApps = Commerce.getInstalledApps(); purchasesModel.getFirstPage(); Commerce.getAvailableUpdates(); @@ -574,13 +572,8 @@ Rectangle { item.cardBackVisible = false; item.isInstalled = root.installedApps.indexOf(item.id) > -1; item.wornEntityID = ''; - item.displayedItemCount = itemCountDictionary[item.id] = (itemCountDictionary[item.id] || 0) + 1; - // HRS FIXME updateable }); sendToScript({ method: 'purchases_updateWearables' }); - for (var i = 0; i < purchasesModel.count; i++) { // Update all the previous counts with possibly new values. - purchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[purchasesModel.get(i).id]) - } return data.assets; @@ -633,7 +626,7 @@ Rectangle { itemEdition: model.edition_number; numberSold: model.number_sold; limitedRun: model.limited_run; - displayedItemCount: model.displayedItemCount || 0; + 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; From 36a0adf553190e560e5235429f2bb48196f02e8e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 14:24:24 -0700 Subject: [PATCH 27/32] strip snapshot stacking code - we'll do it on server or not at all --- interface/resources/qml/hifi/Feed.qml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index cd50eb14d8..0b3dea9e1f 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -41,7 +41,6 @@ Column { // FIXME onFilterChanged: filterChoicesByText(); property var goFunction: null; property var http: null; - property var itemCountDictionary: ({}); HifiConstants { id: hifi } Component.onCompleted: suggestions.getFirstPage(); @@ -56,16 +55,8 @@ Column { ]; endpoint: '/api/v1/user_stories?' + options.join('&'); itemsPerPage: 3; - getFirstPage: function (delayRefresh) { - root.itemCountDictionary = {}; - suggestions.getFirstPageInternal(delayRefresh); - }; processPage: function (data) { - var adding = data.user_stories.map(makeModelData); - for (var i = 0; i < suggestions.count; i++) { // Update all the previous counts with possibly new values. - suggestions.setProperty(i, "drillDownToPlace", itemCountDictionary[suggestions.get(i).place_name] > 0); - } - return adding; + return data.user_stories.map(makeModelData); }; listModelName: actions; listView: scroll; @@ -87,7 +78,6 @@ Column { tags = data.tags || [data.action, data.username], description = data.description || "", thumbnail_url = data.thumbnail_url || ""; - itemCountDictionary[name] = (itemCountDictionary[name] || 0) + 1; return { place_name: name, username: data.username || "", From d9daa3495a1b526164dd620e54a69bcb689099f1 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 15:36:10 -0700 Subject: [PATCH 28/32] remove comments / dead code --- interface/resources/qml/hifi/Feed.qml | 129 +----------------- interface/resources/qml/hifi/Pal.qml | 1 - .../commerce/common/sendAsset/SendAsset.qml | 3 - .../qml/hifi/commerce/purchases/Purchases.qml | 39 +----- .../qml/hifi/commerce/wallet/Wallet.qml | 2 - .../qml/hifi/models/PSFListModel.qml | 7 +- 6 files changed, 5 insertions(+), 176 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 0b3dea9e1f..0b33bac657 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -38,7 +38,6 @@ Column { // 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: ''; - // FIXME onFilterChanged: filterChoicesByText(); property var goFunction: null; property var http: null; @@ -51,7 +50,7 @@ Column { 'include_actions=' + actions, 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), 'require_online=true', - 'protocol=' + Window.protocolSignature() + 'protocol=' + encodeURIComponent(Window.protocolSignature()) ]; endpoint: '/api/v1/user_stories?' + options.join('&'); itemsPerPage: 3; @@ -60,12 +59,7 @@ Column { }; listModelName: actions; listView: scroll; - searchFilter: filter; // FIXME .toUpperCase().split(/\s+/).filter(identity).join(' '); - /* FIXME searchItemTest: function (text, item) { - return searchFilter.split().every(function (word) { - return item.searchText.indexOf(word) >= 0; - }); - };*/ //HRS FIXME remove when endpoint works. + searchFilter: filter; } function resolveUrl(url) { @@ -93,127 +87,8 @@ Column { description: description, online_users: data.details.connections || data.details.concurrency || 0, drillDownToPlace: false - - //searchText: [name].concat(tags, description || []).join(' ').toUpperCase() // FIXME remove }; } - function identity(x) { - return x; - } - /* FIXME - 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; - http.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()'); - //suggestions.getFirstPage(); - } - 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 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); - } - }; - } - function filterChoicesByText() { - suggestions.clear(); - placeMap = {}; - allStories.forEach(makeFilteredStoryProcessor()); - root.visible = !!suggestions.count; - } - */ RalewayBold { id: label; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index f5c47e5042..0735c03a77 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -49,7 +49,6 @@ Rectangle { id: connectionsUserModel; http: http; endpoint: "/api/v1/users?filter=connections"; - //FIXME localSort: true; property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); sortProperty: switch (sortColumn && sortColumn.role) { case 'placeName': diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index b3fd661088..26c0a0aefa 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -404,9 +404,6 @@ Item { return data.users; }; searchFilter: filterBar.text; - /* FIXME searchItemTest: function (text, item) { - return item.username.toLowerCase().indexOf(text.toLowerCase()) !== -1; - };*/ //HRS FIXME remove when endpoint works. } Rectangle { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 886dbbf5d8..3b37edd51b 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -41,7 +41,7 @@ Rectangle { property int numUpdatesAvailable: 0; // Style color: hifi.colors.white; - function getPurchases() { // FIXME: use the new purchasesModel.getFirstPage + function getPurchases() { root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); purchasesModel.getFirstPage(); @@ -576,32 +576,6 @@ Rectangle { sendToScript({ method: 'purchases_updateWearables' }); return data.assets; - - /* - var processedInventory = processInventoryResult(data.assets); - - if (purchasesModel.processResult(result.status, processedInventory)) { - var currentId; - for (var i = 0; i < purchasesModel.count; i++) { - currentId = purchasesModel.get(i).id; - purchasesModel.setProperty(i, 'cardBackVisible', false); - purchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); - purchasesModel.setProperty(i, 'wornEntityID', ''); - } - - // Client-side filter of "Updatable" items - // FIXME - this MUST be serverside (what if we don't have the - // page containing an updatable item on the client?) - if (filterBar.primaryFilter_displayName === "Updatable") { - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).upgrade_url === "") { - purchasesModel.remove(i); - i--; - } - } - } - } - */ } } @@ -979,17 +953,6 @@ Rectangle { // // FUNCTION DEFINITIONS START // - /* fixme remove - function processInventoryResult(inventory) { // HRS FIXME remove - 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 updateCurrentlyWornWearables(wearables) { for (var i = 0; i < purchasesModel.count; i++) { diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index b75141f8dd..819b71a4d0 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -769,12 +769,10 @@ Rectangle { // NOP break; case 'updateConnections': - console.log('Wallet.qml updateConnections');// HRS FIXME sendMoney.updateConnections(message.connections); break; case 'selectRecipient': case 'updateSelectedRecipientUsername': - console.log('Wallet.qml updateSelectedRecipientUsername'); // HRS FIXME sendMoney.fromScript(message); break; case 'http.response': diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index d17689ab98..28ac89c4f7 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -63,7 +63,7 @@ Item { property bool delayedClear: false; function resetModel() { if (!delayedClear) { finalModel.clear(); } - currentPageToRetrieve = 1; console.log('fixme resetModel set currentPageToRetrieve to 1', listModelName); + currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; copyOfItems = []; } @@ -144,7 +144,6 @@ Item { // If it is a path starting with slash, add the metaverseServer domain. var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; var parameters = [ - // FIXME: handle sort, tag parameters 'per_page=' + itemsPerPage, 'page=' + currentPageToRetrieve ]; @@ -162,8 +161,7 @@ Item { // Start the show by retrieving data according to `getPage()`. // It can be custom-defined by this item's Parent. - property var getFirstPage: function (delayClear) { getFirstPageInternal(delayClear); } - function getFirstPageInternal(delayClear) { + property var getFirstPage: function (delayClear) { delayedClear = !!delayClear; resetModel(); requestPending = true; @@ -180,7 +178,6 @@ Item { // onAtYEndChanged: if (theList.atYEnd && !theList.atYBeginning) { thisPSFListModelId.getNextPage(); } // ...} property var getNextPage: function () { - console.log('fixme getNextPage', listModelName, requestPending, currentPageToRetrieve); if (requestPending || currentPageToRetrieve < 0) { return; } From f0d486a6a5ddd0676e58d797551abc02944e3b45 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 15:48:39 -0700 Subject: [PATCH 29/32] remove the unused client-side stuff from model --- .../qml/hifi/models/PSFListModel.qml | 67 ++----------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 28ac89c4f7..68acb09b28 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -38,23 +38,11 @@ Item { onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } onSearchFilterChanged: { if (!initialized) { return; } - if (searchItemTest) { - var filteredCopy = applySearchItemTest(copyOfItems); - finalModel.clear(); - finalModel.append(filteredCopy); - debugView('after searchFilterChanged'); - } else { // TODO: fancy timer against fast typing. - getFirstPage('delayClear'); - } + getFirstPage('delayClear'); } onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); } property int itemsPerPage: 100; - // If the endpoint doesn't do search, tags, sort, these functions can be supplied to do it here. - property var searchItemTest: null; - property bool localSort: false; - property var copyOfItems: []; - // State. property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. property bool retrievedAtLeastOnePage: false; @@ -65,7 +53,6 @@ Item { if (!delayedClear) { finalModel.clear(); } currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; - copyOfItems = []; } // Processing one page. @@ -98,20 +85,7 @@ Item { if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { currentPageToRetrieve = -1; } - if (searchItemTest) { - if (searchFilter) { - processed = applySearchItemTest(processed); - } - copyOfItems = copyOfItems.concat(processed); - } - if (localSort) { - copyOfItems = copyOfItems.concat(processed); - if (sortProperty) { - sortCopy(sortProperty, sortAscending); - processed = copyOfItems; - delayedClear = true; // see next conditional - } - } + if (delayedClear) { finalModel.clear(); delayedClear = false; @@ -119,15 +93,6 @@ Item { finalModel.append(processed); // FIXME keep index steady, and apply any post sort retrievedAtLeastOnePage = true; debugView('after handlePage'); - if (searchItemTest && searchFilter && listView && listView.atYEnd && (currentPageToRetrieve >= 0)) { - getNextPage(); // too fancy?? - } - if (listView) { console.debug('handlePage completed', listModelName, 'model:', model.count, 'view:', listView.count); } - } - function applySearchItemTest(items) { - return items.filter(function (item) { - return searchItemTest(searchFilter, item); - }); } function debugView(label) { if (!listView) { return; } @@ -147,12 +112,13 @@ Item { 'per_page=' + itemsPerPage, 'page=' + currentPageToRetrieve ]; - if (!searchItemTest && searchFilter) { + if (searchFilter) { parameters.splice(parameters.length, 0, 'search=' + searchFilter); } - if (!localSort && sortKey) { + if (sortKey) { parameters.splice(parameters.length, 0, 'sort=' + sortKey); } + var parametersSeparator = /\?/.test(url) ? '&' : '?'; url = url + parametersSeparator + parameters.join('&'); console.debug('getPage', listModelName, currentPageToRetrieve); @@ -202,27 +168,4 @@ Item { ListModel { id: finalModel; } - - function sortCopy(sortProperty, isAscending) { - console.debug('client sort', listModelName, sortProperty, isAscending, copyOfItems.length, 'items'); - var before = isAscending ? -1 : 1; - var after = -1 * before; - - copyOfItems.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; - } - }); - } } \ No newline at end of file From d68415c47cc3b2a86fb69dee1392dfe9a89b1647 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 8 Jun 2018 16:47:06 -0700 Subject: [PATCH 30/32] code feedback --- interface/resources/qml/hifi/Feed.qml | 21 +------------------ interface/resources/qml/hifi/Pal.qml | 7 +++++-- .../commerce/common/sendAsset/SendAsset.qml | 2 +- .../qml/hifi/commerce/purchases/Purchases.qml | 4 ++-- .../qml/hifi/commerce/wallet/Wallet.qml | 3 ++- .../qml/hifi/commerce/wallet/WalletHome.qml | 3 ++- .../qml/hifi/models/PSFListModel.qml | 2 +- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 0b33bac657..d7d469364c 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" -import "models" as HifiModels +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Column { id: root; @@ -136,23 +136,4 @@ Column { unhoverThunk: function () { hovered = false } } } - /* WTF is this? - 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 0735c03a77..d9625a68c2 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -18,7 +18,7 @@ import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControlsUit import "../controls" as HifiControls -import "models" as HifiModels +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. // references HMD, Users, UserActivityLogger from root context @@ -1238,7 +1238,10 @@ Rectangle { reloadNearby.color = 2; } break; - case 'inspectionCertificate_resetCert': // HRS FIXME what's this about? + 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); diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 26c0a0aefa..1353ceab72 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -19,7 +19,7 @@ import "../../../../styles-uit" import "../../../../controls-uit" as HifiControlsUit import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon -import "../../../models" as HifiModels +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 3b37edd51b..ec2798680e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls -import "../../models" as HifiModels +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 @@ -600,7 +600,7 @@ Rectangle { itemEdition: model.edition_number; numberSold: model.number_sold; limitedRun: model.limited_run; - displayedItemCount: 999// For now (and maybe longer), we're going to display all the edition numbers. + 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; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 819b71a4d0..603d7fb676 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -779,7 +779,8 @@ Rectangle { http.handleHttpResponse(message); break; case 'palIsStale': - case 'avatarDisconnected': // HRS FIXME. What are these about? + 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 9076f10ebc..ed032dd055 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -18,7 +18,7 @@ import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls -import "../../models" as HifiModels +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } @@ -229,6 +229,7 @@ Item { } // 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++; diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 68acb09b28..95e9e1b79e 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -55,7 +55,7 @@ Item { retrievedAtLeastOnePage = false; } - // Processing one page. + // 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; } From 421edbfa1e57140fba71b2409a746646e2beb2d0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sat, 9 Jun 2018 16:03:45 -0700 Subject: [PATCH 31/32] PSFListModel isa ListModel instead of hasa --- interface/resources/qml/hifi/Feed.qml | 2 +- interface/resources/qml/hifi/Pal.qml | 2 +- .../commerce/common/sendAsset/SendAsset.qml | 2 +- .../qml/hifi/commerce/purchases/Purchases.qml | 2 +- .../qml/hifi/commerce/wallet/WalletHome.qml | 2 +- .../qml/hifi/models/PSFListModel.qml | 26 ++++--------------- 6 files changed, 10 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index d7d469364c..d9e93c2fa7 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -98,7 +98,7 @@ Column { } ListView { id: scroll; - model: suggestions.model; + model: suggestions; orientation: ListView.Horizontal; highlightFollowsCurrentItem: false highlightMoveDuration: -1; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index d9625a68c2..8dcb76442b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -785,7 +785,7 @@ Rectangle { resizable: false; } - model: connectionsUserModel.model; + model: connectionsUserModel; Connections { target: connectionsTable.flickableItem; onAtYEndChanged: { diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 1353ceab72..3e4bae4780 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -519,7 +519,7 @@ Item { } visible: !connectionsLoading.visible; clip: true; - model: connectionsModel.model; + model: connectionsModel; onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index ec2798680e..c792b88c1e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -583,7 +583,7 @@ Rectangle { id: purchasesContentsList; visible: purchasesModel.count !== 0; clip: true; - model: purchasesModel.model; + model: purchasesModel; snapMode: ListView.SnapToItem; // Anchors anchors.top: separator.bottom; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index ed032dd055..4cf6db7889 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -323,7 +323,7 @@ Item { height: parent.height; visible: transactionHistoryModel.count !== 0; clip: true; - model: transactionHistoryModel.model; + model: transactionHistoryModel; delegate: Item { width: parent.width; height: (model.transaction_type === "pendingCount" && model.count !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 95e9e1b79e..8d1674e1de 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -17,7 +17,7 @@ import QtQuick 2.7 -Item { +ListModel { id: root; // Used when printing debug statements property string listModelName: endpoint; @@ -50,7 +50,7 @@ Item { // Not normally set directly, but rather by giving a truthy argument to getFirstPage(true); property bool delayedClear: false; function resetModel() { - if (!delayedClear) { finalModel.clear(); } + if (!delayedClear) { root.clear(); } currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; } @@ -87,12 +87,12 @@ Item { } if (delayedClear) { - finalModel.clear(); + root.clear(); delayedClear = false; } - finalModel.append(processed); // FIXME keep index steady, and apply any post sort + root.append(processed); // FIXME keep index steady, and apply any post sort retrievedAtLeastOnePage = true; - debugView('after handlePage'); + console.debug(listModelName, 'after handlePage count', root.count); } function debugView(label) { if (!listView) { return; } @@ -152,20 +152,4 @@ Item { requestPending = true; getPage(); } - - // Redefining members and methods so that the parent of this Item - // can use PSFListModel as they would a regular ListModel - property alias model: finalModel; - property alias count: finalModel.count; - function clear() { finalModel.clear(); } - function get(index) { return finalModel.get(index); } - function remove(index) { return finalModel.remove(index); } - function setProperty(index, prop, value) { return finalModel.setProperty(index, prop, value); } - function move(from, to, n) { return finalModel.move(from, to, n); } - function insert(index, newElement) { finalModel.insert(index, newElement); } - function append(newElements) { finalModel.append(newElements); } - - ListModel { - id: finalModel; - } } \ No newline at end of file From 020d62230201acd2f44076344a434ca219f30a84 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sat, 9 Jun 2018 17:34:49 -0700 Subject: [PATCH 32/32] don't depend on matching handlers with overlapping requests --- .../qml/hifi/commerce/purchases/Purchases.qml | 4 +-- .../qml/hifi/models/PSFListModel.qml | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index c792b88c1e..0d2acf4ec3 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -525,7 +525,7 @@ Rectangle { } onPrimaryFilter_displayNameChanged: { - purchasesModel.tagsFilter = filterBar.primaryFilter_filterName.toLowerCase(); + purchasesModel.tagsFilter = filterBar.primaryFilter_filterName; filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName; } @@ -557,7 +557,7 @@ Rectangle { console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); Commerce.inventory( root.isShowingMyItems ? "proofs" : "purchased", - filterBar.primaryFilter_filterName.toLowerCase(), + filterBar.primaryFilter_filterName, filterBar.text, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 8d1674e1de..1bfa2f6ae0 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -36,11 +36,9 @@ ListModel { Component.onCompleted: initialized = true; onEndpointChanged: if (initialized) { getFirstPage('delayClear'); } onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } - onSearchFilterChanged: { - if (!initialized) { return; } - getFirstPage('delayClear'); - } + onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); } onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); } + property int itemsPerPage: 100; // State. @@ -64,7 +62,7 @@ ListModel { // Check consistency and call processPage. function handlePage(error, response) { var processed; - console.debug('handlePage', listModelName, error, JSON.stringify(response)); + console.debug('handlePage', listModelName, additionalFirstPageRequested, error, JSON.stringify(response)); function fail(message) { console.warn("Warning page fail", listModelName, JSON.stringify(message)); currentPageToRetrieve = -1; @@ -92,7 +90,17 @@ ListModel { } root.append(processed); // FIXME keep index steady, and apply any post sort retrievedAtLeastOnePage = true; - console.debug(listModelName, 'after handlePage count', root.count); + // 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; } @@ -128,13 +136,18 @@ ListModel { // 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: