diff --git a/interface/resources/icons/tablet-icons/people-a-msg.svg b/interface/resources/icons/tablet-icons/people-a-msg.svg
new file mode 100644
index 0000000000..862ce936ce
--- /dev/null
+++ b/interface/resources/icons/tablet-icons/people-a-msg.svg
@@ -0,0 +1,83 @@
+
+
+
+
\ No newline at end of file
diff --git a/interface/resources/icons/tablet-icons/people-i-msg.svg b/interface/resources/icons/tablet-icons/people-i-msg.svg
new file mode 100644
index 0000000000..635a01be4b
--- /dev/null
+++ b/interface/resources/icons/tablet-icons/people-i-msg.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml
index 35a0078d32..e8fc41da63 100644
--- a/interface/resources/qml/hifi/Pal.qml
+++ b/interface/resources/qml/hifi/Pal.qml
@@ -271,6 +271,8 @@ Rectangle {
connectionsUserModel.getFirstPage();
}
activeTab = "connectionsTab";
+ connectionsOnlineDot.visible = false;
+ pal.sendToScript({method: 'hideNotificationDot'});
connectionsHelpText.color = hifi.colors.blueAccent;
}
}
@@ -298,6 +300,16 @@ Rectangle {
}
}
}
+ Rectangle {
+ id: connectionsOnlineDot;
+ visible: false;
+ width: 10;
+ height: width;
+ radius: width;
+ color: "#EF3B4E"
+ anchors.left: parent.left;
+ anchors.verticalCenter: parent.verticalCenter;
+ }
// "CONNECTIONS" text
RalewaySemiBold {
id: connectionsTabSelectorText;
@@ -305,7 +317,11 @@ Rectangle {
// Text size
size: hifi.fontSizes.tabularData;
// Anchors
- anchors.fill: parent;
+ anchors.left: connectionsOnlineDot.visible ? connectionsOnlineDot.right : parent.left;
+ anchors.leftMargin: connectionsOnlineDot.visible ? 4 : 0;
+ anchors.top: parent.top;
+ anchors.bottom: parent.bottom;
+ anchors.right: parent.right;
// Style
font.capitalization: Font.AllUppercase;
color: activeTab === "connectionsTab" ? hifi.colors.blueAccent : hifi.colors.baseGray;
@@ -326,7 +342,7 @@ Rectangle {
anchors.left: connectionsTabSelectorTextContainer.left;
anchors.top: connectionsTabSelectorTextContainer.top;
anchors.topMargin: 1;
- anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42;
+ anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42 + connectionsOnlineDot.width + connectionsTabSelectorText.anchors.leftMargin;
RalewayRegular {
id: connectionsHelpText;
text: "[?]";
@@ -1267,6 +1283,9 @@ Rectangle {
case 'http.response':
http.handleHttpResponse(message);
break;
+ case 'changeConnectionsDotStatus':
+ connectionsOnlineDot.visible = message.shouldShowDot;
+ break;
default:
console.log('Unrecognized message:', JSON.stringify(message));
}
diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js
index 3a70a69d4d..02beb41674 100644
--- a/scripts/modules/appUi.js
+++ b/scripts/modules/appUi.js
@@ -11,6 +11,7 @@
//
function AppUi(properties) {
+ var request = Script.require('request').request;
/* Example development order:
1. var AppUi = Script.require('appUi');
2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3).
@@ -80,13 +81,6 @@ function AppUi(properties) {
that.buttonActive = function buttonActive(isActive) { // How to make the button active (white).
that.button.editProperties({isActive: isActive});
};
- that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button.
- // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true.
- that.button.editProperties({
- icon: isWaiting ? that.normalMessagesButton : that.normalButton,
- activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton
- });
- };
that.isQMLUrl = function isQMLUrl(url) {
var type = /.qml$/.test(url) ? 'QML' : 'Web';
return type === 'QML';
@@ -95,6 +89,32 @@ function AppUi(properties) {
return that.currentVisibleScreenType === 'QML';
};
+ //
+ // START Notification Handling Defaults
+ //
+ that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button.
+ // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true.
+ that.button.editProperties({
+ icon: isWaiting ? that.normalMessagesButton : that.normalButton,
+ activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton
+ });
+ };
+ that.notificationPollTimeout = false;
+ that.notificationPollTimeoutMs = 60000;
+ that.notificationPollEndpoint = false;
+ that.notificationPollStopPaginatingConditionMet = false;
+ that.notificationDataProcessPage = function (data) {
+ return data;
+ };
+ that.notificationPollCallback = that.ignore;
+ that.notificationPollCaresAboutSince = false;
+ that.notificationDisplayBanner = function (message) {
+ Window.displayAnnouncement(message);
+ };
+ //
+ // END Notification Handling Defaults
+ //
+
// Handlers
that.onScreenChanged = function onScreenChanged(type, url) {
// Set isOpen, wireEventBridge, set buttonActive as appropriate,
@@ -126,6 +146,75 @@ function AppUi(properties) {
// Overwrite with the given properties:
Object.keys(properties).forEach(function (key) { that[key] = properties[key]; });
+ //
+ // START Notification Handling
+ //
+ var METAVERSE_BASE = Account.metaverseServerURL;
+ var currentDataPageToRetrieve = 1;
+ var concatenatedServerResponse = new Array();
+ that.notificationPoll = function () {
+ if (!that.notificationPollEndpoint) {
+ return;
+ }
+
+ // User is "appearing offline"
+ if (GlobalServices.findableBy === "none") {
+ that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs);
+ return;
+ }
+
+ var url = METAVERSE_BASE + that.notificationPollEndpoint;
+
+ if (that.notificationPollCaresAboutSince) {
+ url = url + "&since=" + (new Date().getTime());
+ }
+
+ console.debug(that.buttonName, 'polling for notifications at endpoint', url);
+
+ function requestCallback(error, response) {
+ if (error || (response.status !== 'success')) {
+ print("Error: unable to get", url, error || response.status);
+ that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs);
+ return;
+ }
+
+ if (!that.notificationPollStopPaginatingConditionMet || that.notificationPollStopPaginatingConditionMet(response)) {
+ that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs);
+
+ var notificationData;
+ if (concatenatedServerResponse.length) {
+ notificationData = concatenatedServerResponse;
+ } else {
+ notificationData = that.notificationDataProcessPage(response);
+ }
+ console.debug(that.buttonName, 'notification data for processing:', JSON.stringify(notificationData));
+ that.notificationPollCallback(notificationData);
+ currentDataPageToRetrieve = 1;
+ concatenatedServerResponse = new Array();
+ } else {
+ concatenatedServerResponse = concatenatedServerResponse.concat(that.notificationDataProcessPage(response));
+ currentDataPageToRetrieve++;
+ request({ uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback);
+ }
+ }
+
+ request({ uri: url }, requestCallback);
+ };
+
+ // This won't do anything if there isn't a notification endpoint set
+ that.notificationPoll();
+
+ function availabilityChanged() {
+ if (that.notificationPollTimeout) {
+ Script.clearTimeout(that.notificationPollTimeout);
+ that.notificationPollTimeout = false;
+ }
+ that.notificationPoll();
+ }
+ //
+ // END Notification Handling
+ //
+
// Properties:
that.tablet = Tablet.getTablet(that.tabletName);
// Must be after we gather properties.
@@ -147,8 +236,9 @@ function AppUi(properties) {
}
that.button = that.tablet.addButton(buttonOptions);
that.ignore = function ignore() { };
- that.hasQmlEventBridge = false;
- that.hasHtmlEventBridge = false;
+ that.hasOutboundEventBridge = false;
+ that.hasInboundQmlEventBridge = false;
+ that.hasInboundHtmlEventBridge = false;
// HTML event bridge uses strings, not objects. Here we abstract over that.
// (Although injected javascript still has to use JSON.stringify/JSON.parse.)
that.sendToHtml = function (messageObject) {
@@ -167,8 +257,10 @@ function AppUi(properties) {
// Outbound (always, regardless of whether there is an inbound handler).
if (on) {
that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml;
+ that.hasOutboundEventBridge = true;
} else {
that.sendMessage = that.ignore;
+ that.hasOutboundEventBridge = false;
}
if (!that.onMessage) {
@@ -177,25 +269,25 @@ function AppUi(properties) {
// Inbound
if (on) {
- if (isCurrentlyOnQMLScreen && !that.hasQmlEventBridge) {
+ if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) {
console.debug(that.buttonName, 'connecting', that.tablet.fromQml);
that.tablet.fromQml.connect(that.onMessage);
- that.hasQmlEventBridge = true;
- } else if (!isCurrentlyOnQMLScreen && !that.hasHtmlEventBridge) {
+ that.hasInboundQmlEventBridge = true;
+ } else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) {
console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived);
that.tablet.webEventReceived.connect(that.fromHtml);
- that.hasHtmlEventBridge = true;
+ that.hasInboundHtmlEventBridge = true;
}
} else {
- if (that.hasQmlEventBridge) {
+ if (that.hasInboundQmlEventBridge) {
console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml);
that.tablet.fromQml.disconnect(that.onMessage);
- that.hasQmlEventBridge = false;
+ that.hasInboundQmlEventBridge = false;
}
- if (that.hasHtmlEventBridge) {
+ if (that.hasInboundHtmlEventBridge) {
console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived);
that.tablet.webEventReceived.disconnect(that.fromHtml);
- that.hasHtmlEventBridge = false;
+ that.hasInboundHtmlEventBridge = false;
}
}
};
@@ -212,6 +304,7 @@ function AppUi(properties) {
} : that.ignore;
that.onScriptEnding = function onScriptEnding() {
// Close if necessary, clean up any remaining handlers, and remove the button.
+ GlobalServices.findableByChanged.disconnect(availabilityChanged);
if (that.isOpen) {
that.close();
}
@@ -222,10 +315,15 @@ function AppUi(properties) {
}
that.tablet.removeButton(that.button);
}
+ if (that.notificationPollTimeout) {
+ Script.clearInterval(that.notificationPollTimeout);
+ that.notificationPollTimeout = false;
+ }
};
// Set up the handlers.
that.tablet.screenChanged.connect(that.onScreenChanged);
that.button.clicked.connect(that.onClicked);
Script.scriptEnding.connect(that.onScriptEnding);
+ GlobalServices.findableByChanged.connect(availabilityChanged);
}
module.exports = AppUi;
diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index 5e38624b35..1abac53f50 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -321,6 +321,10 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
break;
case 'http.request':
break; // Handled by request-service.
+ case 'hideNotificationDot':
+ shouldShowDot = false;
+ ui.messagesWaiting(shouldShowDot);
+ break;
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
}
@@ -364,8 +368,8 @@ function getProfilePicture(username, callback) { // callback(url) if successfull
});
}
var SAFETY_LIMIT = 400;
-function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
- var url = METAVERSE_BASE + '/api/v1/users?per_page=' + SAFETY_LIMIT + '&';
+function getAvailableConnections(domain, callback, numResultsPerPage) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
+ var url = METAVERSE_BASE + '/api/v1/users?per_page=' + (numResultsPerPage || SAFETY_LIMIT) + '&';
if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces
} else {
@@ -728,10 +732,14 @@ function createUpdateInterval() {
var previousContextOverlay = ContextOverlay.enabled;
var previousRequestsDomainListData = Users.requestsDomainListData;
-function on() {
+function palOpened() {
+ ui.sendMessage({
+ method: 'changeConnectionsDotStatus',
+ shouldShowDot: shouldShowDot
+ });
previousContextOverlay = ContextOverlay.enabled;
- previousRequestsDomainListData = Users.requestsDomainListData
+ previousRequestsDomainListData = Users.requestsDomainListData;
ContextOverlay.enabled = false;
Users.requestsDomainListData = true;
@@ -810,14 +818,56 @@ function avatarSessionChanged(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] });
}
+function notificationDataProcessPage(data) {
+ return data.data.users;
+}
+
+var shouldShowDot = false;
+var firstBannerNotificationShown = false;
+function notificationPollCallback(onlineUsersArray) {
+ shouldShowDot = onlineUsersArray.length > 0;
+
+ if (!ui.isOpen) {
+ ui.messagesWaiting(shouldShowDot);
+ ui.sendMessage({
+ method: 'changeConnectionsDotStatus',
+ shouldShowDot: shouldShowDot
+ });
+
+ var message;
+ if (!firstBannerNotificationShown) {
+ message = onlineUsersArray.length + " of your connections are online. Open PEOPLE to join them!";
+ ui.notificationDisplayBanner(message);
+ firstBannerNotificationShown = true;
+ } else {
+ for (var i = 0; i < onlineUsersArray.length; i++) {
+ message = onlineUsersArray[i].username + " is available in " +
+ onlineUsersArray[i].location.root.name + ". Open PEOPLE to join them!";
+ ui.notificationDisplayBanner(message);
+ }
+ }
+ }
+}
+
+function isReturnedDataEmpty(data) {
+ var usersArray = data.data.users;
+ return usersArray.length === 0;
+}
+
function startup() {
ui = new AppUi({
buttonName: "PEOPLE",
sortOrder: 7,
home: "hifi/Pal.qml",
- onOpened: on,
+ onOpened: palOpened,
onClosed: off,
- onMessage: fromQml
+ onMessage: fromQml,
+ notificationPollEndpoint: "/api/v1/users?filter=connections&status=online&per_page=10",
+ notificationPollTimeoutMs: 60000,
+ notificationDataProcessPage: notificationDataProcessPage,
+ notificationPollCallback: notificationPollCallback,
+ notificationPollStopPaginatingConditionMet: isReturnedDataEmpty,
+ notificationPollCaresAboutSince: true
});
Window.domainChanged.connect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL);