Merge pull request #10328 from howard-stearns/tablet-discovery-events

fix announcement suggestions
This commit is contained in:
Howard Stearns 2017-05-01 11:28:45 -07:00 committed by GitHub
commit 9fab66b66a
4 changed files with 137 additions and 87 deletions

View file

@ -34,12 +34,13 @@ Column {
property string metaverseServerUrl: ''; property string metaverseServerUrl: '';
property string actions: 'snapshot'; property string actions: 'snapshot';
onActionsChanged: fillDestinations(); // sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick.
Component.onCompleted: fillDestinations(); Component.onCompleted: delay.start();
property string labelText: actions; property string labelText: actions;
property string filter: ''; property string filter: '';
onFilterChanged: filterChoicesByText(); onFilterChanged: filterChoicesByText();
property var goFunction: null; property var goFunction: null;
property var rpc: null;
HifiConstants { id: hifi } HifiConstants { id: hifi }
ListModel { id: suggestions; } ListModel { id: suggestions; }
@ -81,6 +82,20 @@ Column {
property var allStories: []; property var allStories: [];
property var placeMap: ({}); // Used for making stacks. property var placeMap: ({}); // Used for making stacks.
property int requestId: 0; 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 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. // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness.
var options = [ var options = [
@ -93,8 +108,11 @@ Column {
]; ];
var url = metaverseBase + 'user_stories?' + options.join('&'); var url = metaverseBase + 'user_stories?' + options.join('&');
var thisRequestId = ++requestId; var thisRequestId = ++requestId;
getRequest(url, function (error, data) { rpc('request', url, function (error, data) {
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) { if (thisRequestId !== requestId) {
error = 'stale';
}
if (handleError(url, error, data, cb)) {
return; // abandon stale requests return; // abandon stale requests
} }
allStories = allStories.concat(data.user_stories.map(makeModelData)); allStories = allStories.concat(data.user_stories.map(makeModelData));
@ -107,14 +125,21 @@ Column {
cb(); cb();
}); });
} }
property var delay: Timer { // No setTimeout or nextTick in QML.
interval: 0;
onTriggered: fillDestinations();
}
function fillDestinations() { // Public function fillDestinations() { // Public
function report(label, error) {
console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count);
}
var filter = makeFilteredStoryProcessor(), counter = 0; var filter = makeFilteredStoryProcessor(), counter = 0;
allStories = []; allStories = [];
suggestions.clear(); suggestions.clear();
placeMap = {}; placeMap = {};
getUserStoryPage(1, function (error) { getUserStoryPage(1, function (error) {
allStories.slice(counter).forEach(filter); allStories.slice(counter).forEach(filter);
console.log('user stories query', actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); report('user stories update', error);
root.visible = !!suggestions.count; 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. }, 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) { allStories.forEach(function (story) {
@ -122,15 +147,19 @@ Column {
filter(story); filter(story);
root.visible = !!suggestions.count; root.visible = !!suggestions.count;
}); });
report('user stories');
}); });
} }
function identity(x) {
return x;
}
function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches
var words = filter.toUpperCase().split(/\s+/).filter(identity); var words = filter.toUpperCase().split(/\s+/).filter(identity);
function suggestable(story) { function suggestable(story) {
if (story.action === 'snapshot') { if (story.action === 'snapshot') {
return true; return true;
} }
return (story.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain. return story.place_name !== AddressManager.placename; // Not our entry, but do show other entry points to current domain.
} }
function matches(story) { function matches(story) {
if (!words.length) { if (!words.length) {

View file

@ -33,9 +33,27 @@ StackView {
property int cardWidth: 212; property int cardWidth: 212;
property int cardHeight: 152; property int cardHeight: 152;
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property var tablet: null; 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;
signal sendToScript(var message);
function rpc(method, parameters, callback) {
rpcCalls[rpcCounter] = callback;
var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"};
sendToScript(message);
}
function fromScript(message) {
var callback = rpcCalls[message.id];
if (!callback) {
console.log('No callback for message fromScript', JSON.stringify(message));
return;
}
delete rpcCalls[message.id];
callback(message.error, message.result);
}
Component { id: tabletWebView; TabletWebView {} } Component { id: tabletWebView; TabletWebView {} }
Component.onCompleted: { Component.onCompleted: {
updateLocationText(false); updateLocationText(false);
@ -54,7 +72,7 @@ StackView {
} }
function resetAfterTeleport() { function resetAfterTeleport() {
//storyCardFrame.shown = root.shown = false; //storyCardFrame.shown = root.shown = false;
} }
function goCard(targetString) { function goCard(targetString) {
@ -266,6 +284,7 @@ StackView {
actions: 'announcement'; actions: 'announcement';
filter: addressLine.text; filter: addressLine.text;
goFunction: goCard; goFunction: goCard;
rpc: root.rpc;
} }
Feed { Feed {
id: places; id: places;
@ -278,6 +297,7 @@ StackView {
actions: 'concurrency'; actions: 'concurrency';
filter: addressLine.text; filter: addressLine.text;
goFunction: goCard; goFunction: goCard;
rpc: root.rpc;
} }
Feed { Feed {
id: snapshots; id: snapshots;
@ -291,6 +311,7 @@ StackView {
actions: 'snapshot'; actions: 'snapshot';
filter: addressLine.text; filter: addressLine.text;
goFunction: goCard; goFunction: goCard;
rpc: root.rpc;
} }
} }
} }
@ -330,50 +351,6 @@ StackView {
} }
function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects.
// TODO: make available to other .qml.
var request = new XMLHttpRequest();
// QT bug: apparently doesn't handle onload. Workaround using readyState.
request.onreadystatechange = function () {
var READY_STATE_DONE = 4;
var HTTP_OK = 200;
if (request.readyState >= READY_STATE_DONE) {
var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText,
response = !error && request.responseText,
contentType = !error && request.getResponseHeader('content-type');
if (!error && contentType.indexOf('application/json') === 0) {
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
cb(error, response);
}
};
request.open("GET", url, true);
request.send();
}
function identity(x) {
return x;
}
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 updateLocationText(enteringAddress) { function updateLocationText(enteringAddress) {
if (enteringAddress) { if (enteringAddress) {
notice.text = "Go To a place, @user, path, or network address:"; notice.text = "Go To a place, @user, path, or network address:";

View file

@ -723,7 +723,6 @@ function startup() {
activeIcon: "icons/tablet-icons/people-a.svg", activeIcon: "icons/tablet-icons/people-a.svg",
sortOrder: 7 sortOrder: 7
}); });
tablet.fromQml.connect(fromQml);
button.clicked.connect(onTabletButtonClicked); button.clicked.connect(onTabletButtonClicked);
tablet.screenChanged.connect(onTabletScreenChanged); tablet.screenChanged.connect(onTabletScreenChanged);
Users.usernameFromIDReply.connect(usernameFromIDReply); Users.usernameFromIDReply.connect(usernameFromIDReply);
@ -789,8 +788,23 @@ function onTabletButtonClicked() {
audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS);
} }
} }
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 onTabletScreenChanged(type, url) { function onTabletScreenChanged(type, url) {
wireEventBridge(shouldActivateButton);
// for toolbar mode: change button to active when window is first openend, false otherwise. // for toolbar mode: change button to active when window is first openend, false otherwise.
button.editProperties({isActive: shouldActivateButton}); button.editProperties({isActive: shouldActivateButton});
shouldActivateButton = false; shouldActivateButton = false;

View file

@ -30,40 +30,6 @@
text: buttonName, text: buttonName,
sortOrder: 8 sortOrder: 8
}); });
function messagesWaiting(isWaiting) {
button.editProperties({
icon: isWaiting ? WAITING_ICON : NORMAL_ICON
// No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway.
});
}
function onClicked() {
if (onGotoScreen) {
// for toolbar-mode: go back to home screen, this will close the window.
tablet.gotoHomeScreen();
} else {
shouldActivateButton = true;
tablet.loadQMLSource(gotoQmlSource);
onGotoScreen = true;
}
}
function onScreenChanged(type, url) {
ignore(type);
if (url === gotoQmlSource) {
onGotoScreen = true;
shouldActivateButton = true;
button.editProperties({isActive: shouldActivateButton});
messagesWaiting(false);
} else {
shouldActivateButton = false;
onGotoScreen = false;
button.editProperties({isActive: shouldActivateButton});
}
}
button.clicked.connect(onClicked);
tablet.screenChanged.connect(onScreenChanged);
function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request.
var httpRequest = new XMLHttpRequest(), key; var httpRequest = new XMLHttpRequest(), key;
// QT bug: apparently doesn't handle onload. Workaround using readyState. // QT bug: apparently doesn't handle onload. Workaround using readyState.
@ -112,6 +78,70 @@
httpRequest.open(options.method, options.uri, true); httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body); httpRequest.send(options.body);
} }
function fromQml(message) {
var response = {id: message.id, jsonrpc: "2.0"};
switch (message.method) {
case 'request':
request(message.params, function (error, 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
// No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway.
});
}
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.
tablet.gotoHomeScreen();
} else {
shouldActivateButton = true;
tablet.loadQMLSource(gotoQmlSource);
onGotoScreen = true;
}
}
function onScreenChanged(type, url) {
ignore(type);
if (url === gotoQmlSource) {
onGotoScreen = true;
shouldActivateButton = true;
button.editProperties({isActive: shouldActivateButton});
wireEventBridge(true);
messagesWaiting(false);
} else {
shouldActivateButton = false;
onGotoScreen = false;
button.editProperties({isActive: shouldActivateButton});
wireEventBridge(false);
}
}
button.clicked.connect(onClicked);
tablet.screenChanged.connect(onScreenChanged);
var stories = {}; var stories = {};
var DEBUG = false; var DEBUG = false;