"use strict";//// notifications.js//// Created by Adrian McCarlie on October 8th, 2014// Copyright 2014 High Fidelity, Inc.// Copyright 2022-2024 Overte e.V.//// Display notifications to the user for some specific events.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// SPDX-License-Identifier: Apache-2.0// (function () { Script.include([ "create/audioFeedback/audioFeedback.js" ]); var controllerStandard = Controller.Standard; var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications"; var SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS = "snapshotNotifications"; var NOTIFICATION_LIFE_DURATION = 10000; //10 seconds (in millisecond) before expiration. var FADE_OUT_DURATION = 1000; //1 seconds (in millisecond) to fade out. var NOTIFICATION_ALPHA = 0.9; // a value between: 0.0 (transparent) and 1.0 (fully opaque). var MAX_LINE_LENGTH = 42; var notifications = []; var newEventDetected = false; var isOnHMD = HMD.active; var textColor = { "red": 228, "green": 228, "blue": 228}; var backColor = { "red": 2, "green": 2, "blue": 2}; //DESKTOP OVERLAY PROPERTIES var overlayWidth = 340.0; //width in pixel of notification overlay in desktop var overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0)); // positions window 20px from the right of the interface window var overlayLocationY = 20.0; // position down from top of interface window var overlayTopMargin = 13.0; var overlayLeftMargin = 10.0; var overlayFontSize = 12.0; var TEXT_OVERLAY_FONT_SIZE_IN_PIXELS = 18.0; // taken from TextOverlay::textSize var DESKTOP_INTER_SPACE_NOTIFICATION = 5; //5 px //HMD NOTIFICATION PANEL PROPERTIES var HMD_UI_SCALE_FACTOR = 1.0; //This define the size of all the notification system in HMD. var hmdPanelLocalPosition = {"x": 0.3, "y": 0.25, "z": -1.5}; var hmdPanelLocalRotation = Quat.fromVec3Degrees({"x": 0, "y": -3, "z": 0}); var mainHMDnotificationContainerID = Uuid.NONE; var CAMERA_MATRIX_INDEX = -7; //HMD LOCAL ENTITY PROPERTIES var entityWidth = 0.8; //in meter var HMD_LINE_HEIGHT = 0.03; var entityTopMargin = 0.02; var entityLeftMargin = 0.02; var HMD_INTER_SPACE_NOTIFICATION = 0.05; //ACTIONS // handles clicks on notifications overlays to delete notifications. (DESKTOP only) function mousePressEvent(event) { if (!isOnHMD) { var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); for (var i = 0; i < notifications.length; i += 1) { if (clickedOverlay === notifications[i].overlayID || clickedOverlay === notifications[i].imageOverlayID) { deleteSpecificNotification(i); notifications.splice(i, 1); newEventDetected = true; } } } } function checkHands() { var myLeftHand = Controller.getPoseValue(controllerStandard.LeftHand); var myRightHand = Controller.getPoseValue(controllerStandard.RightHand); var eyesPosition = MyAvatar.getEyePosition(); var hipsPosition = MyAvatar.getJointPosition("Hips"); var eyesRelativeHeight = eyesPosition.y - hipsPosition.y; if (myLeftHand.translation.y > eyesRelativeHeight || myRightHand.translation.y > eyesRelativeHeight) { audioFeedback.action(); deleteAllExistingNotificationsDisplayed(); notifications = []; } } //DISPLAY function renderNotifications(remainingTime) { overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0)); var alpha = NOTIFICATION_ALPHA; if (remainingTime < FADE_OUT_DURATION) { alpha = NOTIFICATION_ALPHA * (remainingTime/FADE_OUT_DURATION); } var properties, count, extraLine, breaks, height; var breakPoint = MAX_LINE_LENGTH + 1; var level = overlayLocationY; var entityLevel = 0; if (notifications.length > 0) { for (var i = 0; i < notifications.length; i++) { count = (notifications[i].dataText.match(/\n/g) || []).length, extraLine = 0; breaks = 0; if (notifications[i].dataText.length >= breakPoint) { breaks = count; } if (isOnHMD) { //use HMD local entities // FIXME: set this to (1 / MyAvatar.sensorToWorldScale) once the // camera joint uses identity scaling like the sensor joint var sensorScaleFactor = HMD_UI_SCALE_FACTOR; var lineHeight = HMD_LINE_HEIGHT; height = lineHeight + (2 * entityTopMargin); extraLine = breaks * lineHeight; height = (height + extraLine) * HMD_UI_SCALE_FACTOR; entityLevel = entityLevel - (height/2); properties = { "type": "Text", "parentID": mainHMDnotificationContainerID, "localPosition": {"x": 0, "y": entityLevel, "z": 0}, "dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01}, "isVisibleInSecondaryCamera": false, "lineHeight": lineHeight * sensorScaleFactor, "textColor": textColor, "textAlpha": alpha, "backgroundColor": backColor, "backgroundAlpha": alpha, "leftMargin": entityLeftMargin * sensorScaleFactor, "topMargin": entityTopMargin * sensorScaleFactor, "unlit": true, // FIXME: The HUD layer is sometimes corrupted or invisible #1439 "renderLayer": "front", "grab": { "grabbable": false }, "ignorePickIntersection": true, }; if (notifications[i].entityID === Uuid.NONE){ properties.text = notifications[i].dataText; notifications[i].entityID = Entities.addEntity(properties, "local"); } else { Entities.editEntity(notifications[i].entityID, properties); } if (notifications[i].dataImage !== null) { entityLevel = entityLevel - (height/2); height = (entityWidth / notifications[i].dataImage.aspectRatio) * HMD_UI_SCALE_FACTOR; entityLevel = entityLevel - (height/2); properties = { "type": "Image", "parentID": mainHMDnotificationContainerID, "localPosition": {"x": 0, "y": entityLevel, "z": 0}, "dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01}, "isVisibleInSecondaryCamera": false, "emissive": true, "visible": true, "alpha": alpha, // FIXME: The HUD layer is sometimes corrupted or invisible #1439 "renderLayer": "front", "grab": { "grabbable": false }, "ignorePickIntersection": true, }; if (notifications[i].imageEntityID === Uuid.NONE){ properties.imageURL = notifications[i].dataImage.path; notifications[i].imageEntityID = Entities.addEntity(properties, "local"); } else { Entities.editEntity(notifications[i].imageEntityID, properties); } } entityLevel = entityLevel - (height/2) - (HMD_INTER_SPACE_NOTIFICATION * HMD_UI_SCALE_FACTOR); } else { //use Desktop overlays height = 40.0; extraLine = breaks * TEXT_OVERLAY_FONT_SIZE_IN_PIXELS; height = height + extraLine; properties = { "x": overlayLocationX, "y": level, "width": overlayWidth, "height": height, "color": textColor, "backgroundColor": backColor, "alpha": alpha, "topMargin": overlayTopMargin, "leftMargin": overlayLeftMargin, "font": {"size": overlayFontSize} }; if (notifications[i].overlayID === Uuid.NONE){ properties.text = notifications[i].dataText; notifications[i].overlayID = Overlays.addOverlay("text", properties); } else { Overlays.editOverlay(notifications[i].overlayID, properties); } if (notifications[i].dataImage !== null) { level = level + height; height = overlayWidth / notifications[i].dataImage.aspectRatio; properties = { "x": overlayLocationX, "y": level, "width": overlayWidth, "height": height, "subImage": { "x": 0, "y": 0 }, "visible": true, "alpha": alpha }; if (notifications[i].imageOverlayID === Uuid.NONE){ properties.imageURL = notifications[i].dataImage.path; notifications[i].imageOverlayID = Overlays.addOverlay("image", properties); } else { Overlays.editOverlay(notifications[i].imageOverlayID, properties); } } level = level + height + DESKTOP_INTER_SPACE_NOTIFICATION; } } } } function deleteAllExistingNotificationsDisplayed() { if (notifications.length > 0) { for (var i = 0; i < notifications.length; i++) { deleteSpecificNotification(i); } } } function deleteSpecificNotification(indexNotification) { if (notifications[indexNotification].entityID !== Uuid.NONE){ Entities.deleteEntity(notifications[indexNotification].entityID); notifications[indexNotification].entityID = Uuid.NONE; } if (notifications[indexNotification].overlayID !== Uuid.NONE){ Overlays.deleteOverlay(notifications[indexNotification].overlayID); notifications[indexNotification].overlayID = Uuid.NONE; } if (notifications[indexNotification].imageEntityID !== Uuid.NONE){ Entities.deleteEntity(notifications[indexNotification].imageEntityID); notifications[indexNotification].imageEntityID = Uuid.NONE; } if (notifications[indexNotification].imageOverlayID !== Uuid.NONE){ Overlays.deleteOverlay(notifications[indexNotification].imageOverlayID); notifications[indexNotification].imageOverlayID = Uuid.NONE; } } function createMainHMDnotificationContainer() { if (mainHMDnotificationContainerID === Uuid.NONE) { var properties = { "name": "Notifications Root", "type": "Empty", "grab": { "grabbable": false }, "ignorePickIntersection": true, "parentID": MyAvatar.sessionUUID, "parentJointIndex": CAMERA_MATRIX_INDEX, "localPosition": hmdPanelLocalPosition, "localRotation": hmdPanelLocalRotation }; mainHMDnotificationContainerID = Entities.addEntity(properties, "local"); } } function deleteMainHMDnotificationContainer() { if (mainHMDnotificationContainerID !== Uuid.NONE) { Entities.deleteEntity(mainHMDnotificationContainerID); mainHMDnotificationContainerID = Uuid.NONE; } } //UTILITY FUNCTIONS // Trims extra whitespace and breaks into lines of length no more // than MAX_LINE_LENGTH, breaking at spaces. Trims extra whitespace. function wordWrap(string) { var finishedLines = [], currentLine = ''; string.split(/\s/).forEach(function (word) { var tail = currentLine ? ' ' + word : word; if ((currentLine.length + tail.length) <= MAX_LINE_LENGTH) { currentLine += tail; } else { finishedLines.push(currentLine); currentLine = word; if (currentLine.length > MAX_LINE_LENGTH) { finishedLines.push(currentLine.substring(0,MAX_LINE_LENGTH)); currentLine = currentLine.substring(MAX_LINE_LENGTH, currentLine.length); } } }); if (currentLine) { finishedLines.push(currentLine); } return finishedLines.join('\n'); } //NOTIFICATION STACK MANAGEMENT function addNotification (dataText, dataImage) { var d = new Date(); var notification = { "dataText": dataText, "dataImage": dataImage, "timestamp": d.getTime(), "entityID": Uuid.NONE, "imageEntityID": Uuid.NONE, "overlayID": Uuid.NONE, "imageOverlayID": Uuid.NONE }; notifications.push(notification); newEventDetected = true; if (notifications.length === 1) { createMainHMDnotificationContainer(); Script.update.connect(update); Controller.mousePressEvent.connect(mousePressEvent); } } function update(deltaTime) { if (notifications.length === 0 && !newEventDetected) { Script.update.disconnect(update); Controller.mousePressEvent.disconnect(mousePressEvent); deleteMainHMDnotificationContainer(); } else { if (isOnHMD !== HMD.active) { deleteAllExistingNotificationsDisplayed(); isOnHMD = HMD.active; } var d = new Date(); var immediatly = d.getTime(); var mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION; var expirationDetected = false; for (var i = 0; i < notifications.length; i++) { if ((immediatly - notifications[i].timestamp) > NOTIFICATION_LIFE_DURATION){ deleteSpecificNotification(i); notifications.splice(i, 1); expirationDetected = true; } else { mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION - (immediatly - notifications[i].timestamp); } } if (newEventDetected || expirationDetected || mostRecentRemainingTime < FADE_OUT_DURATION) { renderNotifications(mostRecentRemainingTime); newEventDetected = false; } } if (isOnHMD) { checkHands(); } } //NOTIFICATION EVENTS FUNCTIONS function onDomainConnectionRefused(reason, reasonCode) { // the "login error" reason means that the DS couldn't decrypt the username signature // since this eventually resolves itself for good actors we don't need to show a notification for it var LoginErrorMetaverse_REASON_CODE = 2; if (reasonCode !== LoginErrorMetaverse_REASON_CODE) { addNotification("Connection refused: " + reason, null); } } function onEditError(msg) { addNotification(wordWrap(msg), null); } function onNotify(msg) { // Generic notification system for user feedback, thus using this addNotification(wordWrap(msg), null); } function onMessageReceived(channel, message) { if (channel === NOTIFICATIONS_MESSAGE_CHANNEL) { message = JSON.parse(message); addNotification(wordWrap(message.message), null); } } function onSnapshotTaken(pathStillSnapshot, notify) { if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) { if (notify) { var imageProperties = { "path": "file:///" + pathStillSnapshot, "aspectRatio": Window.innerWidth / Window.innerHeight }; addNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), imageProperties); } } } function tabletNotification() { addNotification("Tablet needs your attention", null); } function processingGif() { if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) { addNotification("Processing GIF snapshot...", null); } } function connectionAdded(connectionName) { addNotification(connectionName, null); } function connectionError(error) { addNotification(wordWrap("Error trying to make connection: " + error), null); } //STARTING AND ENDING function scriptEnding() { //cleanup deleteAllExistingNotificationsDisplayed(); //disconnecting if (notifications.length > 0) { Script.update.disconnect(update); Controller.mousePressEvent.disconnect(mousePressEvent); } Script.scriptEnding.disconnect(scriptEnding); Messages.unsubscribe(NOTIFICATIONS_MESSAGE_CHANNEL); Window.domainConnectionRefused.disconnect(onDomainConnectionRefused); Window.stillSnapshotTaken.disconnect(onSnapshotTaken); Window.snapshot360Taken.disconnect(onSnapshotTaken); Window.processingGifStarted.disconnect(processingGif); Window.connectionAdded.disconnect(connectionAdded); Window.connectionError.disconnect(connectionError); Window.announcement.disconnect(onNotify); Tablet.tabletNotification.disconnect(tabletNotification); Messages.messageReceived.disconnect(onMessageReceived); } Script.scriptEnding.connect(scriptEnding); //EVENTS TO NOTIFY Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.stillSnapshotTaken.connect(onSnapshotTaken); Window.snapshot360Taken.connect(onSnapshotTaken); Window.processingGifStarted.connect(processingGif); Window.connectionAdded.connect(connectionAdded); Window.connectionError.connect(connectionError); Window.announcement.connect(onNotify); Window.notifyEditError = onEditError; Window.notify = onNotify; Tablet.tabletNotification.connect(tabletNotification); Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL); Messages.messageReceived.connect(onMessageReceived);}());
"use strict";
//
// notifications.js
//
// Created by Adrian McCarlie on October 8th, 2014
// Copyright 2014 High Fidelity, Inc.
// Copyright 2022-2024 Overte e.V.
//
// Display notifications to the user for some specific events.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// SPDX-License-Identifier: Apache-2.0
//
(function () {
Script.include([
"create/audioFeedback/audioFeedback.js"
]);
var controllerStandard = Controller.Standard;
var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications";
var SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS = "snapshotNotifications";
var NOTIFICATION_LIFE_DURATION = 10000; //10 seconds (in millisecond) before expiration.
var FADE_OUT_DURATION = 1000; //1 seconds (in millisecond) to fade out.
var NOTIFICATION_ALPHA = 0.9; // a value between: 0.0 (transparent) and 1.0 (fully opaque).
var MAX_LINE_LENGTH = 42;
var notifications = [];
var newEventDetected = false;
var isOnHMD = HMD.active;
var textColor = { "red": 228, "green": 228, "blue": 228};
var backColor = { "red": 2, "green": 2, "blue": 2};
//DESKTOP OVERLAY PROPERTIES
var overlayWidth = 340.0; //width in pixel of notification overlay in desktop
var overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0)); // positions window 20px from the right of the interface window
var overlayLocationY = 20.0; // position down from top of interface window
var overlayTopMargin = 13.0;
var overlayLeftMargin = 10.0;
var overlayFontSize = 12.0;
var TEXT_OVERLAY_FONT_SIZE_IN_PIXELS = 18.0; // taken from TextOverlay::textSize
var DESKTOP_INTER_SPACE_NOTIFICATION = 5; //5 px
//HMD NOTIFICATION PANEL PROPERTIES
var HMD_UI_SCALE_FACTOR = 1.0; //This define the size of all the notification system in HMD.
var hmdPanelLocalPosition = {"x": 0.3, "y": 0.25, "z": -1.5};
var hmdPanelLocalRotation = Quat.fromVec3Degrees({"x": 0, "y": -3, "z": 0});
var mainHMDnotificationContainerID = Uuid.NONE;
var CAMERA_MATRIX_INDEX = -7;
//HMD LOCAL ENTITY PROPERTIES
var entityWidth = 0.8; //in meter
var HMD_LINE_HEIGHT = 0.03;
var entityTopMargin = 0.02;
var entityLeftMargin = 0.02;
var HMD_INTER_SPACE_NOTIFICATION = 0.05;
//ACTIONS
// handles clicks on notifications overlays to delete notifications. (DESKTOP only)
function mousePressEvent(event) {
if (!isOnHMD) {
var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y });
for (var i = 0; i < notifications.length; i += 1) {
if (clickedOverlay === notifications[i].overlayID || clickedOverlay === notifications[i].imageOverlayID) {
deleteSpecificNotification(i);
notifications.splice(i, 1);
newEventDetected = true;
}
}
}
}
function checkHands() {
var myLeftHand = Controller.getPoseValue(controllerStandard.LeftHand);
var myRightHand = Controller.getPoseValue(controllerStandard.RightHand);
var eyesPosition = MyAvatar.getEyePosition();
var hipsPosition = MyAvatar.getJointPosition("Hips");
var eyesRelativeHeight = eyesPosition.y - hipsPosition.y;
if (myLeftHand.translation.y > eyesRelativeHeight || myRightHand.translation.y > eyesRelativeHeight) {
audioFeedback.action();
deleteAllExistingNotificationsDisplayed();
notifications = [];
}
}
//DISPLAY
function renderNotifications(remainingTime) {
overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0));
var alpha = NOTIFICATION_ALPHA;
if (remainingTime < FADE_OUT_DURATION) {
alpha = NOTIFICATION_ALPHA * (remainingTime/FADE_OUT_DURATION);
}
var properties, count, extraLine, breaks, height;
var breakPoint = MAX_LINE_LENGTH + 1;
var level = overlayLocationY;
var entityLevel = 0;
if (notifications.length > 0) {
for (var i = 0; i < notifications.length; i++) {
count = (notifications[i].dataText.match(/\n/g) || []).length,
extraLine = 0;
breaks = 0;
if (notifications[i].dataText.length >= breakPoint) {
breaks = count;
}
if (isOnHMD) {
//use HMD local entities
// FIXME: set this to (1 / MyAvatar.sensorToWorldScale) once the
// camera joint uses identity scaling like the sensor joint
var sensorScaleFactor = HMD_UI_SCALE_FACTOR;
var lineHeight = HMD_LINE_HEIGHT;
height = lineHeight + (2 * entityTopMargin);
extraLine = breaks * lineHeight;
height = (height + extraLine) * HMD_UI_SCALE_FACTOR;
entityLevel = entityLevel - (height/2);
properties = {
"type": "Text",
"parentID": mainHMDnotificationContainerID,
"localPosition": {"x": 0, "y": entityLevel, "z": 0},
"dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01},
"isVisibleInSecondaryCamera": false,
"lineHeight": lineHeight * sensorScaleFactor,
"textColor": textColor,
"textAlpha": alpha,
"backgroundColor": backColor,
"backgroundAlpha": alpha,
"leftMargin": entityLeftMargin * sensorScaleFactor,
"topMargin": entityTopMargin * sensorScaleFactor,
"unlit": true,
// FIXME: The HUD layer is sometimes corrupted or invisible #1439
"renderLayer": "front",
"grab": { "grabbable": false },
"ignorePickIntersection": true,
};
if (notifications[i].entityID === Uuid.NONE){
properties.text = notifications[i].dataText;
notifications[i].entityID = Entities.addEntity(properties, "local");
} else {
Entities.editEntity(notifications[i].entityID, properties);
}
if (notifications[i].dataImage !== null) {
entityLevel = entityLevel - (height/2);
height = (entityWidth / notifications[i].dataImage.aspectRatio) * HMD_UI_SCALE_FACTOR;
entityLevel = entityLevel - (height/2);
properties = {
"type": "Image",
"parentID": mainHMDnotificationContainerID,
"localPosition": {"x": 0, "y": entityLevel, "z": 0},
"dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01},
"isVisibleInSecondaryCamera": false,
"emissive": true,
"visible": true,
"alpha": alpha,
// FIXME: The HUD layer is sometimes corrupted or invisible #1439
"renderLayer": "front",
"grab": { "grabbable": false },
"ignorePickIntersection": true,
};
if (notifications[i].imageEntityID === Uuid.NONE){
properties.imageURL = notifications[i].dataImage.path;
notifications[i].imageEntityID = Entities.addEntity(properties, "local");
} else {
Entities.editEntity(notifications[i].imageEntityID, properties);
}
}
entityLevel = entityLevel - (height/2) - (HMD_INTER_SPACE_NOTIFICATION * HMD_UI_SCALE_FACTOR);
} else {
//use Desktop overlays
height = 40.0;
extraLine = breaks * TEXT_OVERLAY_FONT_SIZE_IN_PIXELS;
height = height + extraLine;
properties = {
"x": overlayLocationX,
"y": level,
"width": overlayWidth,
"height": height,
"color": textColor,
"backgroundColor": backColor,
"alpha": alpha,
"topMargin": overlayTopMargin,
"leftMargin": overlayLeftMargin,
"font": {"size": overlayFontSize}
};
if (notifications[i].overlayID === Uuid.NONE){
properties.text = notifications[i].dataText;
notifications[i].overlayID = Overlays.addOverlay("text", properties);
} else {
Overlays.editOverlay(notifications[i].overlayID, properties);
}
if (notifications[i].dataImage !== null) {
level = level + height;
height = overlayWidth / notifications[i].dataImage.aspectRatio;
properties = {
"x": overlayLocationX,
"y": level,
"width": overlayWidth,
"height": height,
"subImage": { "x": 0, "y": 0 },
"visible": true,
"alpha": alpha
};
if (notifications[i].imageOverlayID === Uuid.NONE){
properties.imageURL = notifications[i].dataImage.path;
notifications[i].imageOverlayID = Overlays.addOverlay("image", properties);
} else {
Overlays.editOverlay(notifications[i].imageOverlayID, properties);
}
}
level = level + height + DESKTOP_INTER_SPACE_NOTIFICATION;
}
}
}
}
function deleteAllExistingNotificationsDisplayed() {
if (notifications.length > 0) {
for (var i = 0; i < notifications.length; i++) {
deleteSpecificNotification(i);
}
}
}
function deleteSpecificNotification(indexNotification) {
if (notifications[indexNotification].entityID !== Uuid.NONE){
Entities.deleteEntity(notifications[indexNotification].entityID);
notifications[indexNotification].entityID = Uuid.NONE;
}
if (notifications[indexNotification].overlayID !== Uuid.NONE){
Overlays.deleteOverlay(notifications[indexNotification].overlayID);
notifications[indexNotification].overlayID = Uuid.NONE;
}
if (notifications[indexNotification].imageEntityID !== Uuid.NONE){
Entities.deleteEntity(notifications[indexNotification].imageEntityID);
notifications[indexNotification].imageEntityID = Uuid.NONE;
}
if (notifications[indexNotification].imageOverlayID !== Uuid.NONE){
Overlays.deleteOverlay(notifications[indexNotification].imageOverlayID);
notifications[indexNotification].imageOverlayID = Uuid.NONE;
}
}
function createMainHMDnotificationContainer() {
if (mainHMDnotificationContainerID === Uuid.NONE) {
var properties = {
"name": "Notifications Root",
"type": "Empty",
"grab": { "grabbable": false },
"ignorePickIntersection": true,
"parentID": MyAvatar.sessionUUID,
"parentJointIndex": CAMERA_MATRIX_INDEX,
"localPosition": hmdPanelLocalPosition,
"localRotation": hmdPanelLocalRotation
};
mainHMDnotificationContainerID = Entities.addEntity(properties, "local");
}
}
function deleteMainHMDnotificationContainer() {
if (mainHMDnotificationContainerID !== Uuid.NONE) {
Entities.deleteEntity(mainHMDnotificationContainerID);
mainHMDnotificationContainerID = Uuid.NONE;
}
}
//UTILITY FUNCTIONS
// Trims extra whitespace and breaks into lines of length no more
// than MAX_LINE_LENGTH, breaking at spaces. Trims extra whitespace.
function wordWrap(string) {
var finishedLines = [], currentLine = '';
string.split(/\s/).forEach(function (word) {
var tail = currentLine ? ' ' + word : word;
if ((currentLine.length + tail.length) <= MAX_LINE_LENGTH) {
currentLine += tail;
} else {
finishedLines.push(currentLine);
currentLine = word;
if (currentLine.length > MAX_LINE_LENGTH) {
finishedLines.push(currentLine.substring(0,MAX_LINE_LENGTH));
currentLine = currentLine.substring(MAX_LINE_LENGTH, currentLine.length);
}
}
});
if (currentLine) {
finishedLines.push(currentLine);
}
return finishedLines.join('\n');
}
//NOTIFICATION STACK MANAGEMENT
function addNotification (dataText, dataImage) {
var d = new Date();
var notification = {
"dataText": dataText,
"dataImage": dataImage,
"timestamp": d.getTime(),
"entityID": Uuid.NONE,
"imageEntityID": Uuid.NONE,
"overlayID": Uuid.NONE,
"imageOverlayID": Uuid.NONE
};
notifications.push(notification);
newEventDetected = true;
if (notifications.length === 1) {
createMainHMDnotificationContainer();
Script.update.connect(update);
Controller.mousePressEvent.connect(mousePressEvent);
}
}
function update(deltaTime) {
if (notifications.length === 0 && !newEventDetected) {
Script.update.disconnect(update);
Controller.mousePressEvent.disconnect(mousePressEvent);
deleteMainHMDnotificationContainer();
} else {
if (isOnHMD !== HMD.active) {
deleteAllExistingNotificationsDisplayed();
isOnHMD = HMD.active;
}
var d = new Date();
var immediatly = d.getTime();
var mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION;
var expirationDetected = false;
for (var i = 0; i < notifications.length; i++) {
if ((immediatly - notifications[i].timestamp) > NOTIFICATION_LIFE_DURATION){
deleteSpecificNotification(i);
notifications.splice(i, 1);
expirationDetected = true;
} else {
mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION - (immediatly - notifications[i].timestamp);
}
}
if (newEventDetected || expirationDetected || mostRecentRemainingTime < FADE_OUT_DURATION) {
renderNotifications(mostRecentRemainingTime);
newEventDetected = false;
}
}
if (isOnHMD) {
checkHands();
}
}
//NOTIFICATION EVENTS FUNCTIONS
function onDomainConnectionRefused(reason, reasonCode) {
// the "login error" reason means that the DS couldn't decrypt the username signature
// since this eventually resolves itself for good actors we don't need to show a notification for it
var LoginErrorMetaverse_REASON_CODE = 2;
if (reasonCode !== LoginErrorMetaverse_REASON_CODE) {
addNotification("Connection refused: " + reason, null);
}
}
function onEditError(msg) {
addNotification(wordWrap(msg), null);
}
function onNotify(msg) {
// Generic notification system for user feedback, thus using this
addNotification(wordWrap(msg), null);
}
function onMessageReceived(channel, message) {
if (channel === NOTIFICATIONS_MESSAGE_CHANNEL) {
message = JSON.parse(message);
addNotification(wordWrap(message.message), null);
}
}
function onSnapshotTaken(pathStillSnapshot, notify) {
if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) {
if (notify) {
var imageProperties = {
"path": "file:///" + pathStillSnapshot,
"aspectRatio": Window.innerWidth / Window.innerHeight
};
addNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), imageProperties);
}
}
}
function tabletNotification() {
addNotification("Tablet needs your attention", null);
}
function processingGif() {
if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) {
addNotification("Processing GIF snapshot...", null);
}
}
function connectionAdded(connectionName) {
addNotification(connectionName, null);
}
function connectionError(error) {
addNotification(wordWrap("Error trying to make connection: " + error), null);
}
//STARTING AND ENDING
function scriptEnding() {
//cleanup
deleteAllExistingNotificationsDisplayed();
//disconnecting
if (notifications.length > 0) {
Script.update.disconnect(update);
Controller.mousePressEvent.disconnect(mousePressEvent);
}
Script.scriptEnding.disconnect(scriptEnding);
Messages.unsubscribe(NOTIFICATIONS_MESSAGE_CHANNEL);
Window.domainConnectionRefused.disconnect(onDomainConnectionRefused);
Window.stillSnapshotTaken.disconnect(onSnapshotTaken);
Window.snapshot360Taken.disconnect(onSnapshotTaken);
Window.processingGifStarted.disconnect(processingGif);
Window.connectionAdded.disconnect(connectionAdded);
Window.connectionError.disconnect(connectionError);
Window.announcement.disconnect(onNotify);
Tablet.tabletNotification.disconnect(tabletNotification);
Messages.messageReceived.disconnect(onMessageReceived);
}
Script.scriptEnding.connect(scriptEnding);
//EVENTS TO NOTIFY
Window.domainConnectionRefused.connect(onDomainConnectionRefused);
Window.stillSnapshotTaken.connect(onSnapshotTaken);
Window.snapshot360Taken.connect(onSnapshotTaken);
Window.processingGifStarted.connect(processingGif);
Window.connectionAdded.connect(connectionAdded);
Window.connectionError.connect(connectionError);
Window.announcement.connect(onNotify);
Window.notifyEditError = onEditError;
Window.notify = onNotify;
Tablet.tabletNotification.connect(tabletNotification);
Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL);
Messages.messageReceived.connect(onMessageReceived);
}());