Implemented chat interface script and tablet web page for worklist #21280

This commit is contained in:
Don Hopkins 2017-05-04 21:40:11 +02:00
parent 7cb2bff4c5
commit a9c1e2781e
2 changed files with 1501 additions and 0 deletions

View file

@ -0,0 +1,990 @@
"use strict";
// Chat.js
// By Don Hopkins (dhopkins@donhopkins.com)
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function() {
var webPageURL = "ChatPage.html"; // URL of tablet web page.
var randomizeWebPageURL = true; // Set to true for debugging.
var lastWebPageURL = ""; // Last random URL of tablet web page.
var onChatPage = false; // True when chat web page is opened.
var webHandlerConnected = false; // True when the web handler has been connected.
var channelName = "Chat"; // Unique name for channel that we listen to.
var tabletButtonName = "CHAT"; // Tablet button label.
var tabletButtonIcon = "icons/tablet-icons/menu-i.svg"; // Icon for chat button.
var tabletButtonActiveIcon = "icons/tablet-icons/menu-a.svg"; // Active icon for chat button.
var tabletButton = null; // The button we create in the tablet.
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // The awesome tablet.
var chatLog = []; // Array of chat messages in the form of [avatarID, displayName, message, data].
var avatarIdentifiers = {}; // Map of avatar ids to dict of identifierParams.
var speechBubbleShowing = false; // Is the speech bubble visible?
var speechBubbleMessage = null; // The message shown in the speech bubble.
var speechBubbleData = null; // The data of the speech bubble message.
var speechBubbleTextID = null; // The id of the speech bubble local text entity.
var speechBubbleTimer = null; // The timer to pop down the speech bubble.
var speechBubbleParams = null; // The params used to create or edit the speech bubble.
// Persistent variables saved in the Settings.
var chatName = ''; // The user's name shown in chat.
var chatLogMaxSize = 100; // The maximum number of chat messages we remember.
var sendTyping = true; // Send typing begin and end notification.
var identifyAvatarDuration = 10; // How long to leave the avatar identity line up, in seconds.
var identifyAvatarLineColor = { red: 0, green: 255, blue: 0 }; // The color of the avatar identity line.
var identifyAvatarMyJointName = 'Head'; // My bone from which to draw the avatar identity line.
var identifyAvatarYourJointName = 'Head'; // Your bone to which to draw the avatar identity line.
var speechBubbleDuration = 10; // How long to leave the speech bubble up, in seconds.
var speechBubbleTextColor = {red: 255, green: 255, blue: 255}; // The text color of the speech bubble.
var speechBubbleBackgroundColor = {red: 0, green: 0, blue: 0}; // The background color of the speech bubble.
var speechBubbleOffset = {x: 0, y: 0.3, z: 0.0}; // The offset from the joint to whic the speech bubble is attached.
var speechBubbleJointName = 'Head'; // The name of the joint to which the speech bubble is attached.
var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble.
// Load the persistent variables from the Settings, with defaults.
function loadSettings() {
chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName);
print("loadSettings: chatName", chatName);
if (!chatName) {
chatName = randomAvatarName();
}
print("loadSettings: now chatName", chatName);
chatLogMaxSize = Settings.getValue('Chat_chatLogMaxSize', 100);
sendTyping = Settings.getValue('Chat_sendTyping', true);
identifyAvatarDuration = Settings.getValue('Chat_identifyAvatarDuration', 10);
identifyAvatarLineColor = Settings.getValue('Chat_identifyAvatarLineColor', { red: 0, green: 255, blue: 0 });
identifyAvatarMyJointName = Settings.getValue('Chat_identifyAvatarMyJointName', 'Head');
identifyAvatarYourJointName = Settings.getValue('Chat_identifyAvatarYourJointName', 'Head');
speechBubbleDuration = Settings.getValue('Chat_speechBubbleDuration', 10);
speechBubbleTextColor = Settings.getValue('Chat_speechBubbleTextColor', {red: 255, green: 255, blue: 255});
speechBubbleBackgroundColor = Settings.getValue('Chat_speechBubbleBackgroundColor', {red: 0, green: 0, blue: 0});
speechBubbleOffset = Settings.getValue('Chat_speechBubbleOffset', {x: 0.0, y: 0.3, z:0.0});
speechBubbleJointName = Settings.getValue('Chat_speechBubbleJointName', 'Head');
speechBubbleLineHeight = Settings.getValue('Chat_speechBubbleLineHeight', 0.05);
saveSettings();
}
// Save the persistent variables to the Settings.
function saveSettings() {
Settings.setValue('Chat_chatName', chatName);
print("saveSettings: chatName", chatName, "or", Settings.getValue('Chat_chatName', 'xxx'));
Settings.setValue('Chat_chatLogMaxSize', chatLogMaxSize);
Settings.setValue('Chat_sendTyping', sendTyping);
Settings.setValue('Chat_identifyAvatarDuration', identifyAvatarDuration);
Settings.setValue('Chat_identifyAvatarLineColor', identifyAvatarLineColor);
Settings.setValue('Chat_identifyAvatarMyJointName', identifyAvatarMyJointName);
Settings.setValue('Chat_identifyAvatarYourJointName', identifyAvatarYourJointName);
Settings.setValue('Chat_speechBubbleDuration', speechBubbleDuration);
Settings.setValue('Chat_speechBubbleTextColor', speechBubbleTextColor);
Settings.setValue('Chat_speechBubbleBackgroundColor', speechBubbleBackgroundColor);
Settings.setValue('Chat_speechBubbleOffset', speechBubbleOffset);
Settings.setValue('Chat_speechBubbleJointName', speechBubbleJointName);
Settings.setValue('Chat_speechBubbleLineHeight', speechBubbleLineHeight);
}
// Reset the Settings and persistent variables to the defaults.
function resetSettings() {
Settings.setValue('Chat_chatName', null);
Settings.setValue('Chat_chatLogMaxSize', null);
Settings.setValue('Chat_sendTyping', null);
Settings.setValue('Chat_identifyAvatarDuration', null);
Settings.setValue('Chat_identifyAvatarLineColor', null);
Settings.setValue('Chat_identifyAvatarMyJointName', null);
Settings.setValue('Chat_identifyAvatarYourJointName', null);
Settings.setValue('Chat_speechBubbleDuration', null);
Settings.setValue('Chat_speechBubbleTextColor', null);
Settings.setValue('Chat_speechBubbleBackgroundColor', null);
Settings.setValue('Chat_speechBubbleOffset', null);
Settings.setValue('Chat_speechBubbleJointName', null);
Settings.setValue('Chat_speechBubbleLineHeight', null);
loadSettings();
}
// Update anything that might depend on the settings.
function updateSettings() {
updateSpeechBubble();
trimChatLog();
updateChatPage();
}
// Trim the chat log so it is no longer than chatLogMaxSize lines.
function trimChatLog() {
if (chatLog.length > chatLogMaxSize) {
chatLog.splice(0, chatLogMaxSize - chatLog.length);
}
}
// Clear the local chat log.
function clearChatLog() {
//print("clearChatLog");
chatLog = [];
updateChatPage();
}
// We got a chat message from the channel.
// Trim the chat log, save the latest message in the chat log,
// and show the message on the tablet, if the chat page is showing.
function handleTransmitChatMessage(avatarID, displayName, message, data) {
//print("receiveChat", "avatarID", avatarID, "displayName", displayName, "message", message, "data", data);
trimChatLog();
chatLog.push([avatarID, displayName, message, data]);
if (onChatPage) {
tablet.emitScriptEvent(
JSON.stringify({
type: "ReceiveChatMessage",
avatarID: avatarID,
displayName: displayName,
message: message,
data: data
}));
}
}
// Trim the chat log, save the latest log message in the chat log,
// and show the message on the tablet, if the chat page is showing.
function logMessage(message, data) {
//print("logMessage", message, data);
trimChatLog();
chatLog.push([null, null, message, data]);
if (onChatPage) {
tablet.emitScriptEvent(
JSON.stringify({
type: "LogMessage",
message: message,
data: data
}));
}
}
// An empty chat message was entered.
// Hide our speech bubble.
function emptyChatMessage(data) {
popDownSpeechBubble();
}
// Notification that we typed a keystroke.
function type() {
//print("type");
}
// Notification that we began typing.
// Notify everyone that we started typing.
function beginTyping() {
//print("beginTyping");
if (!sendTyping) {
return;
}
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'AvatarBeginTyping',
avatarID: MyAvatar.sessionUUID,
displayName: chatName
}));
}
// Notification that somebody started typing.
function handleAvatarBeginTyping(avatarID, displayName) {
print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName);
}
// Notification that we stopped typing.
// Notify everyone that we stopped typing.
function endTyping() {
//print("endTyping");
if (!sendTyping) {
return;
}
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'AvatarEndTyping',
avatarID: MyAvatar.sessionUUID,
displayName: chatName
}));
}
// Notification that somebody stopped typing.
function handleAvatarEndTyping(avatarID, displayName) {
print("handleAvatarEndTyping:", "avatarID", avatarID, displayName);
}
// Identify an avatar by drawing a line from our head to their head.
// If the avatar is our own, then just draw a line up into the sky.
function identifyAvatar(yourAvatarID) {
//print("identifyAvatar", yourAvatarID);
unidentifyAvatars();
var myAvatarID = MyAvatar.sessionUUID;
var myJointIndex = MyAvatar.getJointIndex(identifyAvatarMyJointName);
var myJointRotation =
Quat.multiply(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointRotationInObjectFrame(myJointIndex));
var myJointPosition =
Vec3.sum(
MyAvatar.position,
Vec3.multiplyQbyV(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointTranslationInObjectFrame(myJointIndex)));
var yourJointIndex = -1;
var yourJointPosition;
if (yourAvatarID == myAvatarID) {
// You pointed at your own name, so draw a line up from your head.
yourJointPosition = {
x: myJointPosition.x,
y: myJointPosition.y + 1000.0,
z: myJointPosition.z
};
} else {
// You pointed at somebody else's name, so draw a line from your head to their head.
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
if (!yourAvatar) {
return;
}
yourJointIndex = yourAvatar.getJointIndex(identifyAvatarMyJointName)
var yourJointRotation =
Quat.multiply(
yourAvatar.orientation,
yourAvatar.getAbsoluteJointRotationInObjectFrame(yourJointIndex));
yourJointPosition =
Vec3.sum(
yourAvatar.position,
Vec3.multiplyQbyV(
yourAvatar.orientation,
yourAvatar.getAbsoluteJointTranslationInObjectFrame(yourJointIndex)));
}
var identifierParams = {
parentID: myAvatarID,
parentJointIndex: myJointIndex,
lifetime: identityAvatarDuration,
start: myJointPosition,
endParentID: yourAvatarID,
endParentJointIndex: yourJointIndex,
end: yourJointPosition,
color: identifyAvatarLineColor,
alpha: 1,
lineWidth: 1
};
avatarIdentifiers[yourAvatarID] = identifierParams;
identifierParams.lineID = Overlays.addOverlay("line3d", identifierParams);
//print("ADDOVERLAY lineID", lineID, "myJointPosition", JSON.stringify(myJointPosition), "yourJointPosition", JSON.stringify(yourJointPosition), "lineData", JSON.stringify(lineData));
identifierParams.timer =
Script.setTimeout(function() {
//print("DELETEOVERLAY lineID");
unidentifyAvatar(yourAvatarID);
}, identifyAvatarDuration * 1000);
}
// Stop identifying an avatar.
function unidentifyAvatar(yourAvatarID) {
//print("unidentifyAvatar", yourAvatarID);
var identifierParams = avatarIdentifiers[yourAvatarID];
if (!identifierParams) {
return;
}
if (identifierParams.timer) {
Script.clearTimeout(identifierParams.timer);
}
if (identifierParams.lineID) {
Overlays.deleteOverlay(identifierParams.lineID);
}
delete avatarIdentifiers[yourAvatarID];
}
// Stop identifying all avatars.
function unidentifyAvatars() {
var ids = [];
for (var avatarID in avatarIdentifiers) {
ids.push(avatarID);
}
for (var i = 0, n = ids.length; i < n; i++) {
var avatarID = ids[i];
unidentifyAvatar(avatarID);
}
}
// Turn to face another avatar.
function faceAvatar(yourAvatarID, displayName) {
//print("faceAvatar:", yourAvatarID, displayName);
var myAvatarID = MyAvatar.sessionUUID;
if (yourAvatarID == myAvatarID) {
// You clicked on yourself.
return;
}
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
if (!yourAvatar) {
logMessage(displayName + ' is not here!', null);
return;
}
// Project avatar positions to the floor and get the direction between those points,
// then face my avatar towards your avatar.
var yourPosition = yourAvatar.position;
yourPosition.y = 0;
var myPosition = MyAvatar.position;
myPosition.y = 0;
var myOrientation = Quat.lookAtSimple(myPosition, yourPosition);
MyAvatar.orientation = myOrientation;
}
// Make a hopefully unique random anonymous avatar name.
function randomAvatarName() {
return 'Anon_' + Math.floor(Math.random() * 1000000);
}
// Change the avatar size to bigger.
function biggerSize() {
//print("biggerSize");
logMessage("Increasing avatar size bigger!", null);
MyAvatar.increaseSize();
}
// Change the avatar size to smaller.
function smallerSize() {
//print("smallerSize");
logMessage("Decreasing avatar size smaler!", null);
MyAvatar.decreaseSize();
}
// Set the avatar size to normal.
function normalSize() {
//print("normalSize");
logMessage("Resetting avatar size to normal!", null);
MyAvatar.resetSize();
}
// Send out a "Who" message, including our avatarID as myAvatarID,
// which will be sent in the response, so we can tell the reply
// is to our request.
function transmitWho() {
//print("transmitWho");
logMessage("Who is here?", null);
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'Who',
myAvatarID: MyAvatar.sessionUUID
}));
}
// Send a reply to a "Who" message, with a friendly message,
// our avatarID and our displayName. myAvatarID is the id
// of the avatar who send the Who message, to whom we're
// responding.
function handleWho(myAvatarID) {
var avatarID = MyAvatar.sessionUUID;
if (myAvatarID == avatarID) {
// Don't reply to myself.
return;
}
var message = "I'm here!";
var data = {};
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'ReplyWho',
myAvatarID: myAvatarID,
avatarID: avatarID,
displayName: chatName,
message: message,
data: data
}));
}
// Receive the reply to a "Who" message. Ignore it unless we were the one
// who sent it out (if myAvatarIS is our avatar's id).
function handleReplyWho(myAvatarID, avatarID, displayName, message, data) {
if (myAvatarID != MyAvatar.sessionUUID) {
return;
}
receiveChatMessageTablet(avatarID, displayName, message, data);
}
// Handle input form the user, possibly multiple lines separated by newlines.
// Each line may be a chat command starting with "/", or a chat message.
function handleChatMessage(message, data) {
var messageLines = message.trim().split('\n');
for (var i = 0, n = messageLines.length; i < n; i++) {
var messageLine = messageLines[i];
if (messageLine.substr(0, 1) == '/') {
handleChatCommand(messageLine, data);
} else {
transmitChatMessage(messageLine, data);
}
}
}
// Handle a chat command prefixed by "/".
function handleChatCommand(message, data) {
var commandLine = message.substr(1);
var tokens = commandLine.trim().split(' ');
var command = tokens[0];
var rest = commandLine.substr(command.length + 1).trim();
//print("commandLine", commandLine, "command", command, "tokens", tokens, "rest", rest);
switch (command) {
case '?':
case 'help':
logMessage('Type "/?" or "/help" for help, which is this!', null);
logMessage('Type "/name <name>" to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null);
logMessage('Type "/shutup" to shut up your overhead chat message.', null);
logMessage('Type "/say <something>" to say something.', null);
logMessage('Type "/clear" to clear your cha, nullt log.', null);
logMessage('Type "/who" to ask who is h, nullere to chat.', null);
logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null);
logMessage('(Sorry, that\'s all there is so far!)', null);
break;
case 'name':
if (rest == '') {
if (MyAvatar.displayName) {
chatName = MyAvatar.displayName;
saveSettings();
logMessage('Your chat name has been set to your display name "' + chatName + '".', null);
} else {
chatName = randomAvatarName();
saveSettings();
logMessage('Your avatar\'s display name is not defined, so your chat name has been set to "' + chatName + '".', null);
}
} else {
chatName = rest;
saveSettings();
logMessage('Your chat name has been set to "' + chatName + '".', null);
}
break;
case 'shutup':
popDownSpeechBubble();
logMessage('Overhead chat message shut up.', null);
break;
case 'say':
if (rest == '') {
emptyChatMessage(data);
} else {
transmitChatMessage(rest, data);
}
break;
case 'who':
transmitWho();
break;
case 'clear':
clearChatLog();
break;
case 'bigger':
biggerSize();
break;
case 'smaller':
smallerSize();
break;
case 'normal':
normalSize();
break;
case 'resetsettings':
resetSettings();
updateSettings();
break;
case 'speechbubbleheight':
var y = parseInt(rest);
if (!isNaN(y)) {
speechBubbleOffset.y = y;
}
saveSettings();
updateSettings();
break;
case 'speechbubbleduration':
var duration = parseFloat(rest);
if (!isNaN(duration)) {
speechBubbleDuration = duration;
}
saveSettings();
updateSettings();
break;
default:
logMessage('Unknown chat command. Type "/help" or "/?" for help.', null);
break;
}
}
// Send out a chat message to everyone.
function transmitChatMessage(message, data) {
//print("transmitChatMessage", 'avatarID', avatarID, 'displayName', displayName, 'message', message, 'data', data);
popUpSpeechBubble(message, data);
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'TransmitChatMessage',
avatarID: MyAvatar.sessionUUID,
displayName: chatName,
message: message,
data: data
}));
}
// Show the speech bubble.
function popUpSpeechBubble(message, data) {
//print("popUpSpeechBubble", message, data);
popDownSpeechBubble();
speechBubbleShowing = true;
speechBubbleMessage = message;
speechBubbleData = data;
updateSpeechBubble();
if (speechBubbleDuration > 0) {
speechBubbleTimer = Script.setTimeout(
function () {
popDownSpeechBubble();
},
speechBubbleDuration * 1000);
}
}
// Update the speech bubble.
// This is factored out so we can update an existing speech bubble if any settings change.
function updateSpeechBubble() {
if (!speechBubbleShowing) {
return;
}
var jointIndex = MyAvatar.getJointIndex(speechBubbleJointName);
var dimensions = {
x: 100.0,
y: 100.0,
z: 0.1
};
speechBubbleParams = {
type: "Text",
lifetime: speechBubbleDuration,
parentID: MyAvatar.sessionUUID,
jointIndex: jointIndex,
dimensions: dimensions,
lineHeight: speechBubbleLineHeight,
leftMargin: 0,
topMargin: 0,
rightMargin: 0,
bottomMargin: 0,
faceCamera: true,
drawInFront: true,
ignoreRayIntersection: true,
text: speechBubbleMessage,
textColor: speechBubbleTextColor,
color: speechBubbleTextColor,
backgroundColor: speechBubbleBackgroundColor
};
// Only overlay text3d has a way to measure the text, not entities.
// So we make a temporary one just for measuring text, then delete it.
var speechBubbleTextOverlayID = Overlays.addOverlay("text3d", speechBubbleParams);
var textSize = Overlays.textSize(speechBubbleTextOverlayID, speechBubbleMessage);
try {
Overlays.deleteOverlay(speechBubbleTextOverlayID);
} catch (e) {}
print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height);
var fudge = 0.02;
var width = textSize.width + fudge;
var height = textSize.height + fudge;
dimensions = {
x: width,
y: height,
z: 0.1
};
speechBubbleParams.dimensions = dimensions;
var headRotation =
Quat.multiply(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex));
var headPosition =
Vec3.sum(
MyAvatar.position,
Vec3.multiplyQbyV(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)));
var rotatedOffset =
Vec3.multiplyQbyV(
headRotation,
speechBubbleOffset);
var position =
Vec3.sum(
headPosition,
rotatedOffset);
speechBubbleParams.position = position;
if (!speechBubbleTextID) {
speechBubbleTextID =
Entities.addEntity(speechBubbleParams, true);
} else {
Entities.editEntity(speechBubbleTextID, speechBubbleParams);
}
print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams));
}
// Hide the speech bubble.
function popDownSpeechBubble() {
cancelSpeechBubbleTimer();
speechBubbleShowing = false;
print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID);
if (speechBubbleTextID) {
try {
Entities.deleteEntity(speechBubbleTextID);
} catch (e) {}
speechBubbleTextID = null;
}
}
// Cancel the speech bubble popup timer.
function cancelSpeechBubbleTimer() {
if (speechBubbleTimer) {
Script.clearTimeout(speechBubbleTimer);
speechBubbleTimer = null;
}
}
// Show the tablet web page and connect the web handler.
function showTabletWebPage() {
var url = Script.resolvePath(webPageURL);
if (randomizeWebPageURL) {
url += '?rand=' + Math.random();
}
lastWebPageURL = url;
onChatPage = true;
tablet.gotoWebScreen(lastWebPageURL);
// Connect immediately so we don't miss anything.
connectWebHandler();
}
// Update the tablet web page with the chat log.
function updateChatPage() {
if (!onChatPage) {
return;
}
tablet.emitScriptEvent(
JSON.stringify({
type: "Update",
chatLog: chatLog
}));
}
function onChatMessageReceived(channel, message, senderID) {
// Ignore messages to any other channel than mine.
if (channel != channelName) {
return;
}
// Parse the message and pull out the message parameters.
var messageData = JSON.parse(message);
var messageType = messageData.type;
//print("MESSAGE", message);
//print("MESSAGEDATA", messageData, JSON.stringify(messageData));
switch (messageType) {
case 'TransmitChatMessage':
handleTransmitChatMessage(messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
break;
case 'AvatarBeginTyping':
handleAvatarBeginTyping(messageData.avatarID, messageData.displayName);
break;
case 'AvatarEndTyping':
handleAvatarEndTyping(messageData.avatarID, messageData.displayName);
break;
case 'Who':
handleWho(messageData.myAvatarID);
break;
case 'ReplyWho':
handleReplyWho(messageData.myAvatarID, messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
break;
default:
print("onChatMessageReceived: unknown messageType", messageType, "message", message);
break;
}
}
// Handle events from the tablet web page.
function onWebEventReceived(event) {
if (!onChatPage) {
return;
}
//print("onWebEventReceived: event", event);
var eventData = JSON.parse(event);
var eventType = eventData.type;
switch (eventType) {
case 'Ready':
updateChatPage();
break;
case 'Update':
updateChatPage();
break;
case 'HandleChatMessage':
var message = eventData.message;
var data = eventData.data;
//print("onWebEventReceived: HandleChatMessage:", 'message', message, 'data', data);
handleChatMessage(message, data);
break;
case 'PopDownSpeechBubble':
popDownSpeechBubble();
break;
case 'EmptyChatMessage':
emptyChatMessage();
break;
case 'Type':
type();
break;
case 'BeginTyping':
beginTyping();
break;
case 'EndTyping':
endTyping();
break;
case 'IdentifyAvatar':
identifyAvatar(eventData.avatarID);
break;
case 'UnidentifyAvatar':
unidentifyAvatar(eventData.avatarID);
break;
case 'FaceAvatar':
faceAvatar(eventData.avatarID, eventData.displayName);
break;
case 'ClearChatLog':
clearChatLog();
break;
case 'Who':
transmitWho();
break;
case 'Bigger':
biggerSize();
break;
case 'Smaller':
smallerSize();
break;
case 'Normal':
normalSize();
break;
default:
print("onWebEventReceived: unexpected eventType", eventType);
break;
}
}
function onScreenChanged(type, url) {
//print("onScreenChanged", "type", type, "url", url, "lastWebPageURL", lastWebPageURL);
if ((type === "Web") &&
(url === lastWebPageURL)) {
if (!onChatPage) {
onChatPage = true;
connectWebHandler();
}
} else {
if (onChatPage) {
onChatPage = false;
disconnectWebHandler();
}
}
}
function connectWebHandler() {
if (webHandlerConnected) {
return;
}
try {
tablet.webEventReceived.connect(onWebEventReceived);
} catch (e) {
print("connectWebHandler: error connecting: " + e);
return;
}
webHandlerConnected = true;
//print("connectWebHandler connected");
updateChatPage();
}
function disconnectWebHandler() {
if (!webHandlerConnected) {
return;
}
try {
tablet.webEventReceived.disconnect(onWebEventReceived);
} catch (e) {
print("disconnectWebHandler: error disconnecting web handler: " + e);
return;
}
webHandlerConnected = false;
//print("disconnectWebHandler: disconnected");
}
// Show the tablet web page when the chat button on the tablet is clicked.
function onTabletButtonClicked() {
showTabletWebPage();
}
// Shut down the chat application when the tablet button is destroyed.
function onTabletButtonDestroyed() {
shutDown();
}
// Start up the chat application.
function startUp() {
//print("startUp");
loadSettings();
tabletButton = tablet.addButton({
icon: tabletButtonIcon,
activeIcon: tabletButtonActiveIcon,
text: tabletButtonName,
sortOrder: 0
});
Messages.subscribe(channelName);
tablet.screenChanged.connect(onScreenChanged);
Messages.messageReceived.connect(onChatMessageReceived);
tabletButton.clicked.connect(onTabletButtonClicked);
Script.scriptEnding.connect(onTabletButtonDestroyed);
logMessage('Type "/?" or "/help" for help with chat.', null);
//print("Added chat button to tablet.");
}
// Shut down the chat application.
function shutDown() {
//print("shutDown");
popDownSpeechBubble();
unidentifyAvatars();
disconnectWebHandler();
if (onChatPage) {
tablet.gotoHomeScreen();
onChatPage = false;
}
tablet.screenChanged.disconnect(onScreenChanged);
Messages.messageReceived.disconnect(onChatMessageReceived);
// Clean up the tablet button we made.
tabletButton.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(tabletButton);
tabletButton = null;
//print("Removed chat button from tablet.");
}
// Kick off the chat application!
startUp();
}());

View file

@ -0,0 +1,511 @@
<!--
// ChatPage.html
//
// Created by Faye Li on 3 Feb 2017
// Modified by Don Hopkins (dhopkins@donhopkins.com).
// Copyright 2017 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
-->
<html>
<head>
<title>Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600,700"" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<style>
input[type=button],
button {
font-family: 'Raleway';
font-weight: bold;
font-size: 20px;
vertical-align: top;
width: 100%;
height: 40px;
min-width: 120px;
padding: 0px 18px;
margin-top: 5px;
margin-right: 6px;
border-radius: 5px;
border: none;
color: #fff;
background-color: #000;
background: linear-gradient(#343434 20%, #000 100%);
cursor: pointer;
}
.commandButton {
width: 100px !important;
}
input[type=text] {
font-family: 'Raleway';
font-weight: bold;
font-size: 20px;
vertical-align: top;
height: 40px;
color: #000;
width: 100%;
background-color: #fff;
background: linear-gradient(#343434 20%, #fff 100%);
}
input[type=button].red {
color: #fff;
background-color: #94132e;
background: linear-gradient(#d42043 20%, #94132e 100%);
}
input[type=button].blue {
color: #fff;
background-color: #1080b8;
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
}
input[type=button].white {
color: #121212;
background-color: #afafaf;
background: linear-gradient(#fff 20%, #afafaf 100%);
}
input[type=button]:enabled:hover {
background: linear-gradient(#000, #000);
border: none;
}
input[type=button].red:enabled:hover {
background: linear-gradient(#d42043, #d42043);
border: none;
}
input[type=button].blue:enabled:hover {
background: linear-gradient(#00b4ef, #00b4ef);
border: none;
}
input[type=button].white:enabled:hover {
background: linear-gradient(#fff, #fff);
border: none;
}
input[type=button]:active {
background: linear-gradient(#343434, #343434);
}
input[type=button].red:active {
background: linear-gradient(#94132e, #94132e);
}
input[type=button].blue:active {
background: linear-gradient(#1080b8, #1080b8);
}
input[type=button].white:active {
background: linear-gradient(#afafaf, #afafaf);
}
input[type=button]:disabled {
color: #252525;
background: linear-gradient(#575757 20%, #252525 100%);
}
input[type=button][pressed=pressed] {
color: #00b4ef;
}
body {
width: 100%;
overflow-x: hidden;
overflow-y: hidden;
margin: 0;
font-family: 'Raleway', sans-serif;
color: white;
background: linear-gradient(#2b2b2b, #0f212e);
}
.Content {
font-size: 20px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.TopBar {
height: 40px;
background: linear-gradient(#2b2b2b, #1e1e1e);
font-weight: bold;
padding: 10px 10px 10px 10px;
display: flex;
align-items: center;
width: 100%;
font-size: 28px;
flex-grow: 0;
}
.ChatLog {
padding: 20px;
font-size: 20px;
flex-grow: 1;
color: white;
background-color: black;
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
}
.ChatLogLine {
margin-bottom: 15px;
}
.ChatLogLineDisplayName {
font-weight: bold;
}
.ChatLogLineMessage {
}
.LogLogLine {
margin-bottom: 15px;
}
.LogLogLineMessage {
font-style: italic;
}
.ChatInput {
display: flex;
flex-direction: row;
flex-grow: 0;
}
.ChatInputText {
padding: 20px 20px 20px 20px;
height: 60px !important;
font-size: 20px !important;
flex-grow: 1;
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div class="Content">
<div class="TopBar">
<b>Chat</b>
</div>
<div class="ChatLog" id="ChatLog"></div>
<div class="ChatInput">
<input type="text" class="ChatInputText" id="ChatInputText" size="256"/>
</div>
</div>
</body>
<script>
//console.log("ChatPage: loading script...");
var messageData = {}; // The data that is sent along with the message.
var typing = false; // True while the user is typing.
var typingTimerDuration = 1; // How long to wait before ending typing, in seconds.
var typingTimer = null; // The timer to end typing.
var $ChatLog; // The scrolling chat log.
var $ChatInputText; // The text field for entering text.
// Recreate the lines in chatLog as the DOM in $ChatLog.
function updateChatLog() {
$ChatLog.html('');
for (var i = 0, n = chatLog.length; i < n; i++) {
var a = chatLog[i];
var avatarID = a[0];
var displayName = a[1];
var message = a[2];
var data = a[3];
//console.log("updateChatLog", i, a, displayName, message);
if (avatarID) {
receiveChatMessage(avatarID, displayName, message, data);
} else {
logMessage(message);
}
}
}
// Call this no every keystroke.
function type() {
beginTyping();
handleType();
}
// Reset the typing timer, and notify if we're starting.
function beginTyping() {
if (typingTimer) {
clearTimeout(typingTimer);
}
typingTimer = setTimeout(function() {
typing = false;
handleEndTyping();
}, typingTimerDuration * 1000);
if (typing) {
return;
}
typing = true;
handleBeginTyping();
}
// Clear the typing timer and notify if we're finished.
function endTyping() {
if (typingTimer) {
clearTimeout(typingTimer);
typingTimer = null;
}
if (!typing) {
return;
}
typing = false;
handleEndTyping();
}
// Notify the interface script on every keystroke.
function handleType() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "Type"
}));
}
// Notify the interface script when we begin typing.
function handleBeginTyping() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "BeginTyping"
}));
}
// Notify the interface script when we end typing.
function handleEndTyping() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "EndTyping"
}));
}
// Append a chat message to $ChatLog.
function receiveChatMessage(avatarID, displayName, message, data) {
var $logLine =
$('<div/>')
.addClass('ChatLogLine')
.data('chat-avatarID', avatarID)
.data('chat-displayName', displayName)
.data('chat-message', message)
.data('chat-data', data)
.appendTo($ChatLog);
var $logLineDisplayName =
$('<span/>')
.addClass('ChatLogLineDisplayName')
.text(displayName + ': ')
.on('mouseenter', function(event) {
identifyAvatar(avatarID);
//event.cancelBubble();
$logLineDisplayName.css({
color: '#00ff00',
'text-decoration': 'underline'
});
})
.on('mouseleave', function(event) {
unidentifyAvatar(avatarID);
//event.cancelBubble();
$logLineDisplayName.css({
color: 'white',
'text-decoration': 'none'
});
})
.click(function(event) {
faceAvatar(avatarID, displayName);
//event.cancelBubble();
})
.appendTo($logLine);
var $logLineMessage =
$('<span/>')
.addClass('ChatLogLineMessage')
.text(message)
.appendTo($logLine);
}
// Append a log message to $ChatLog.
function logMessage(message) {
var $logLine =
$('<div/>')
.addClass('LogLogLine')
.data('chat-message', message)
.appendTo($ChatLog);
var $logLineMessage =
$('<span/>')
.addClass('LogLogLineMessage')
.text(message)
.appendTo($logLine);
}
// Scroll $ChatLog so the last line is visible.
function scrollChatLog() {
var $logLine = $ChatLog.children().last();
if (!$logLine || !$logLine.length) {
return;
}
var chatLogHeight = $ChatLog.outerHeight(true);
var logLineTop = ($logLine.offset().top - $ChatLog.offset().top);
var logLineBottom = logLineTop + $logLine.outerHeight(true);
var scrollUp = logLineBottom - chatLogHeight;
if (scrollUp > 0) {
$ChatLog.scrollTop($ChatLog.scrollTop() + scrollUp);
}
}
// Tell the interface script we have initialized.
function emitReadyEvent() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "Ready"
}));
}
// The user entered an empty chat message.
function emptyChatMessage() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "EmptyChatMessage",
data: null
}));
}
// The user entered a non-empty chat message.
function handleChatMessage(message, data) {
//console.log("handleChatMessage", message);
EventBridge.emitWebEvent(
JSON.stringify({
type: "HandleChatMessage",
message: message,
data: data
}));
}
// Clear the chat log, of course.
function clearChatLog() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "ClearChatLog"
}));
}
// Identify an avatar.
function identifyAvatar(avatarID) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "IdentifyAvatar",
avatarID: avatarID
}));
}
// Stop identifying an avatar.
function unidentifyAvatar(avatarID) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "UnidentifyAvatar",
avatarID: avatarID
}));
}
// Face an avatar.
function faceAvatar(avatarID, displayName) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "FaceAvatar",
avatarID: avatarID,
displayName: displayName
}));
}
// Let's get this show on the road!
function main() {
//console.log("ChatPage: main");
$ChatLog = $('#ChatLog');
$ChatInputText = $('#ChatInputText');
// Whenever the chat log resizes, or the input text gets or loses focus,
// scroll the chat log to the last line.
$ChatLog.on('resize', function(event) {
//console.log("ChatLog resize", $ChatLog, event);
scrollChatLog();
});
$ChatInputText.on('focus blur', function(event) {
//console.log("ChatInputText focus blur", $ChatInputText, event);
scrollChatLog();
});
// Track when the user is typing, and handle the message when the user hits return.
$ChatInputText.on('keydown', function(event) {
type();
if (event.keyCode == 13) {
var message = $ChatInputText.val().substr(0, 256);
$ChatInputText.val('');
if (message == '') {
emptyChatMessage();
} else {
handleChatMessage(message, messageData);
}
endTyping();
}
});
// Start out with the input text in focus.
$ChatInputText.focus();
// Hook up a handler for events that come from hifi.
EventBridge.scriptEventReceived.connect(function (message) {
//console.log("ChatPage: main: scriptEventReceived", message);
var messageData = JSON.parse(message);
var messageType = messageData['type'];
switch (messageType) {
case "Update":
chatLog = messageData['chatLog'];
updateChatLog();
scrollChatLog();
break;
case "ReceiveChatMessage":
receiveChatMessage(messageData['avatarID'], messageData['displayName'], messageData['message'], message['data']);
scrollChatLog();
break;
case "LogMessage":
logMessage(messageData['message']);
scrollChatLog();
break;
default:
console.log("WEB: received unexpected script event message: " + message);
break;
}
});
emitReadyEvent();
}
// Start up once the document is loaded.
$(document).ready(main);
</script>
</html>