From 67dca7639fe4f28673a26ee4b89a2005ece63801 Mon Sep 17 00:00:00 2001
From: Marcus Llewellyn <marcus.llewellyn@gmail.com>
Date: Wed, 11 Dec 2019 12:25:43 -0600
Subject: [PATCH] Adds community script directory, adds chat as default app.

This adds a community directory under the scripts directory. George Deacon's chat app has been placed within, and is loaded as a default script.
---
 scripts/community/chat.js | 995 ++++++++++++++++++++++++++++++++++++++
 scripts/defaultScripts.js |   4 +-
 2 files changed, 997 insertions(+), 2 deletions(-)
 create mode 100644 scripts/community/chat.js

diff --git a/scripts/community/chat.js b/scripts/community/chat.js
new file mode 100644
index 0000000000..a2301611c4
--- /dev/null
+++ b/scripts/community/chat.js
@@ -0,0 +1,995 @@
+"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 = Script.resolvePath("html/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.
+    var SPEECH_BUBBLE_MAX_WIDTH = 1; // meters
+
+    // Load the persistent variables from the Settings, with defaults.
+    function loadSettings() {
+        chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName);
+        if (!chatName) {
+            chatName = randomAvatarName();
+        }
+        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);
+        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: identifyAvatarDuration,
+            start: myJointPosition,
+            endParentID: yourAvatarID,
+            endParentJointIndex: yourJointIndex,
+            end: yourJointPosition,
+            color: identifyAvatarLineColor,
+            alpha: 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", null);
+        MyAvatar.increaseSize();
+    }
+
+    // Change the avatar size to smaller.
+    function smallerSize() {
+        //print("smallerSize");
+        logMessage("Decreasing avatar size", 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;
+        }
+
+        handleTransmitChatMessage(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', null);
+                logMessage('Type "/name <name>" to set your chat name, or "/name" to use your display name. If your display name is not defined, a random name will be used.', null);
+                logMessage('Type "/close" to close your overhead chat message.', null);
+                logMessage('Type "/say <something>" to display a new message.', null);
+                logMessage('Type "/clear" to clear your chat log.', null);
+                logMessage('Type "/who" to ask who is in the chat session.', null);
+                logMessage('Type "/bigger", "/smaller" or "/normal" to change your avatar size.', 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 'close':
+                popDownSpeechBubble();
+                logMessage('Overhead chat message closed.', 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 = speechBubbleLineHeight + fudge;
+
+        if (textSize.width >= SPEECH_BUBBLE_MAX_WIDTH) {
+            var numLines = Math.ceil(width);
+            height = speechBubbleLineHeight * numLines + fudge;
+            width = SPEECH_BUBBLE_MAX_WIDTH;
+        } 
+
+        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);
+        position.y += height / 2; // offset based on half of bubble height
+        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
+        });
+
+        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();
+
+}());
diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index 7f78d2477f..491ac117b6 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -34,11 +34,11 @@ var DEFAULT_SCRIPTS_COMBINED = [
     "system/emote.js",
     "system/miniTablet.js",
     "system/audioMuteOverlay.js",
-    "system/keyboardShortcuts/keyboardShortcuts.js"
+    "system/keyboardShortcuts/keyboardShortcuts.js",
+    "community/chat.js"
 ];
 var DEFAULT_SCRIPTS_SEPARATE = [
     "system/controllers/controllerScripts.js"
-    //"system/chat.js"
 ];
 
 if (Window.interstitialModeEnabled) {