/* Copyright (c) 2008, Katharine Berry
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Katharine Berry nor the names of any contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY KATHARINE BERRY ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL KATHARINE BERRY BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
// IM Window Handler
AjaxLife.InstantMessage = function() {
// Private
var dialog = false;
var activesession = false;
var width = 700;
var height = 400;
var chats = {};
var highlighted = new Array();
var origtabcolour = false;
var highlightcolour = 'red';
var highlight = false;
var friendlist = false;
var noted_typing = false;
var grouplist = false;
var groups = {};
function fillgroups(data)
{
for(var key in data.Groups)
{
var group = data.Groups[key];
groups[key] = group;
AjaxLife.NameCache.AddGroup(key,group.Name);
grouplist.add(key,group.Name);
}
}
// Set a tab to flash
function highlighttab(sessionid)
{
if(chats[sessionid] && highlighted.indexOf(sessionid) == -1)
{
origtabcolour = chats[sessionid].tab.textEl.getStyle('color');
highlighted[highlighted.length] = sessionid;
}
};
// Set a tab to stop flashing
function unhighlight(sessionid)
{
highlighted = highlighted.without(sessionid);
if(chats[sessionid]) chats[sessionid].tab.textEl.setStyle({color: origtabcolour});
};
// Turn the text of a tab red or not red, alternating each time it's called.
function processhighlight()
{
highlight = !highlight;
highlighted.each(function(item) {
// Check that the tab's still there. If it's not, remove it from the array.
if(chats[item])
{
chats[item].tab.textEl.setStyle({color: highlight?highlightcolour:origtabcolour});
}
else
{
highlighted = highlighted.without(item);
}
});
};
// Handles resized tabs.
function fixtab(sessionid)
{
if(chats[sessionid])
{
chats[sessionid].content.dom.scrollTop = chats[sessionid].content.dom.scrollHeight;
chats[sessionid].content.setStyle({height: (height - 88)+'px'});
chats[sessionid].entrybox.setStyle({width: (width - 133)+'px'});
}
};
// Sends an IM saying the target, using the sessionid.
// NOTE: sessionid should be the same each time an agent is messaged - otherwise the IM
// will appear in a different tab in the official client.
function sendmessage(target, message, sessionid)
{
if(message.blank())
{
return;
}
if(sessionid == null)
{
sessionid = AjaxLife.Utils.UUID.Random();
}
// Notify other person that typing has stopped (unless we're in a group chat)
if(!chats[sessionid].groupIM)
{
AjaxLife.Network.Send('GenericInstantMessage', {
Message: "none",
Target: chats[sessionid].target,
IMSessionID: sessionid,
Online: AjaxLife.Constants.MainAvatar.InstantMessageOnline.Online,
Dialog: AjaxLife.Constants.MainAvatar.InstantMessageDialog.StopTyping
});
AjaxLife.Network.Send("SimpleInstantMessage", {
IMSessionID: sessionid,
Target: chats[sessionid].groupIM ? sessionid : target,
Message: message
});
// Add the IM to your own window, being sure to handle /me correctly.
if(message.substr(0,3) == "/me")
{
message = gUserName+message.substr(3);
}
else
{
message = gUserName+": "+message;
}
appendline(sessionid, message, {name: gUserName, id: gAgentID});
}
else
{
AjaxLife.Network.Send("GroupInstantMessage", {
Message: message,
Group: sessionid
});
}
noted_typing = false;
};
// Creates a new IM session with agent "id" who is called "name".
// Session ID should be generated such that all IMs with the target will have the same ID,
// but IMs from different people to the same agent, or the same person to different agents, will not.
function createTab(id, name, sessionid, groupIM)
{
if(!groupIM) groupIM = false; // Avoid differences between false and undefined.
AjaxLife.Debug("InstantMessage: Creating session "+sessionid+" with "+id+" ("+name+"; groupIM = "+groupIM+")");
// Create the tab and add to the array.
chats[sessionid] = {
tab: dialog.getTabs().addTab("im-"+sessionid, (groupIM ? "(hippos)" : name), "", true),
name: name,
target: id,
content: false,
entrybox: false,
sendbtn: false,
div_typing: false,
session: sessionid,
groupIM: groupIM
};
if(groupIM)
{
AjaxLife.Debug("InstantMessage: Looking up group "+sessionid+"...");
AjaxLife.NameCache.FindGroup(sessionid, function(groupname) {
AjaxLife.Debug("InstantMessage: Found group name: "+groupname);
chats[sessionid].name = groupname;
chats[sessionid].tab.setText(groupname);
});
}
chats[sessionid].tab.on('close',function() {
// Send message informing that we have left the conversation.
AjaxLife.Network.Send('GenericInstantMessage', {
Message: "",
Target: id,
IMSessionID: sessionid,
Online: AjaxLife.Constants.MainAvatar.InstantMessageOnline.Online,
Dialog: AjaxLife.Constants.MainAvatar.InstantMessageDialog.SessionDrop
});
if(dialog.getTabs().getActiveTab() && dialog.getTabs().getActiveTab().id == chats[sessionid].tab.id)
{
activesession = false;
}
delete chats[sessionid];
});
chats[sessionid].tab.bodyEl.setStyle({'overflow': 'hidden'});
// Chat area
var content = Ext.get(document.createElement('div'));
content.setStyle({overflow: 'auto', width:'99%'});
chats[sessionid].content = content;
chats[sessionid].tab.bodyEl.dom.appendChild(content.dom);
var entrybox = new AjaxLife.Widgets.ChatEntryBox(chats[sessionid].tab.bodyEl.dom, 'im-input-'+sessionid, function(text) { sendmessage(id, text, sessionid); }, {height: '20px'});
chats[sessionid].entrybox = entrybox;
chats[sessionid].tab.bodyEl.setStyle({overflow: 'hidden'});
// Button setup, callbacks and formatting.
var style = {position: 'absolute', bottom: '0px', right: '0px'};
chats[sessionid].sendbtn = new Ext.Button(chats[sessionid].tab.bodyEl, {
handler: function() {
sendmessage(id, entrybox.getValue(), chats[sessionid].session);
entrybox.resetLine();
},
text: _("InstantMessage.Send")
});
chats[sessionid].sendbtn.getEl().setStyle(style);
style.right = '48px';
// We can't do group profiles yet.
if(!groupIM)
{
(new Ext.Button(chats[sessionid].tab.bodyEl, {
handler: function() {
new AjaxLife.Profile(chats[sessionid].target);
},
text: _("InstantMessage.Profile")
})).getEl().setStyle(style);
}
div_typing = Ext.get(document.createElement('div'));
div_typing.addClass(['chatline','agenttyping']);
div_typing.dom.appendChild(document.createTextNode(_("InstantMessage.Typing",{name: name})));
chats[sessionid].div_typing = div_typing;
// None of the "... is typing" stuff works in group IMs.
if(!groupIM)
{
// Called two seconds after the last key is pressed. Sends not typing notification.
var delayed_stop_typing = new Ext.util.DelayedTask(function() {
// Make sure that this session still exists first.
if(!chats[sessionid]) return;
AjaxLife.Network.Send('GenericInstantMessage', {
Message: "none",
Target: chats[sessionid].target,
IMSessionID: chats[sessionid].session,
Online: AjaxLife.Constants.MainAvatar.InstantMessageOnline.Online,
Dialog: AjaxLife.Constants.MainAvatar.InstantMessageDialog.StopTyping
});
noted_typing = false;
});
// Sends typing notification and sets timeout for above function to two seconds.
entrybox.addListener('keypress',function(e) {
if(!noted_typing)
{
noted_typing = true;
AjaxLife.Network.Send('GenericInstantMessage', {
Message: "none",
Target: chats[sessionid].target,
IMSessionID: chats[sessionid].session,
Online: AjaxLife.Constants.MainAvatar.InstantMessageOnline.Online,
Dialog: AjaxLife.Constants.MainAvatar.InstantMessageDialog.StartTyping
});
}
delayed_stop_typing.delay(2000);
});
}
chats[sessionid].tab.on('activate',function() {
unhighlight(sessionid);
activesession = sessionid;
fixtab(sessionid);
entrybox.focus();
});
var currenttab = dialog.getTabs().getActiveTab().id;
// These are essentially contentless, so switch IM window if we're activated and on one of these.
if(currenttab == 'im-default-tab' || currenttab == 'im-group-tab')
{
chats[sessionid].tab.activate();
}
return true;
};
// Append a line to the box with a timestamp.
function appendline(session, text, agent)
{
if(chats[session] && chats[session].content)
{
text = AjaxLife.Utils.LinkifyText(text);
if(agent && agent.name && agent.id && agent.id != AjaxLife.Utils.UUID.Zero)
{
text = text.sub(agent.name, ''+agent.name+'');
}
var line = Ext.get(document.createElement('div'));
line.addClass(["agentmessage","chatline"]);
var timestamp = Ext.get(document.createElement('span'));
timestamp.addClass("chattimestamp");
var time = new Date();
timestamp.dom.appendChild(document.createTextNode("["+time.getHours()+":"+((time.getMinutes()<10)?("0"+time.getMinutes()):time.getMinutes())+"]"));
line.dom.appendChild(timestamp.dom);
line.dom.appendChild(document.createTextNode(" "));
var span = document.createElement('span');
span.innerHTML = text;
line.dom.appendChild(span);
chats[session].content.dom.appendChild(line.dom);
// Scroll to the end.
chats[session].content.dom.scrollTop = chats[session].content.dom.scrollHeight;
}
else
{
AjaxLife.Widgets.Ext.msg("Warning","Instant message with unknown ID {0}:
{1}",session,text);
}
};
function joingroupchat(group)
{
AjaxLife.Network.Send("GenericInstantMessage", {
Message: "",
Target: group,
IMSessionID: group,
Online: AjaxLife.Constants.MainAvatar.InstantMessageOnline.Online,
Dialog: AjaxLife.Constants.MainAvatar.InstantMessageDialog.SessionGroupStart
});
}
return {
// Public
init: function () {
// Create the new window at 700x400, with a default tab for friendlist.
dialog = new Ext.BasicDialog("dlg_im", {
height: 400,
width: 700,
minHeight: 100,
minWidth: 150,
modal: false,
shadow: true,
autoCreate: true,
title: _("InstantMessage.WindowTitle"),
proxyDrag: !AjaxLife.Fancy
});
dialog.getTabs().addTab("im-default-tab",_("InstantMessage.OnlineFriends"),"",false).activate();
friendlist = new AjaxLife.Widgets.SelectList('im-friend-list',dialog.getTabs().getActiveTab().bodyEl.dom,{
width: '99%',
callback: function(key) {
AjaxLife.NameCache.Find(key, function(name) {
createTab(key, name, AjaxLife.Utils.UUID.Combine(gAgentID,key));
});
}
});
var sortdelay = new Ext.util.DelayedTask(function() {
friendlist.sort();
});
// Deal with adding and removing friends to/from the friend list.
var addname = function (friend) {
if(friend.Online)
{
friendlist.add(friend.ID,friend.Name);
}
else
{
friendlist.remove(friend.ID);
}
sortdelay.delay(200);
};
AjaxLife.Friends.AddStatusCallback(addname);
AjaxLife.Friends.AddNewFriendCallback(addname);
dialog.body.setStyle({overflow: 'hidden'});
width = 700;
height = 400;
dialog.on('resize', function(d, w, h) {
width = w;
height = h;
fixtab(activesession);
});
var grouptab = dialog.getTabs().addTab("im-group-tab",_("InstantMessage.Groups"), "", false);
grouplist = new AjaxLife.Widgets.SelectList("im-group-list", grouptab.bodyEl.dom, {
width: '99%',
callback: function(key) {
joingroupchat(key);
createTab(key, key, key, true);
chats[key].entrybox.disable();
chats[key].sendbtn.getEl().dom.enabled = false;
}
});
// Handle successfully started chats.
AjaxLife.Network.MessageQueue.RegisterCallback('ChatGroupJoin', function(data) {
var group = data.GroupChatSessionID;
if(chats[group] && !chats[group].entrybox.isEnabled())
{
if(data.Success)
{
chats[group].entrybox.enable();
chats[group].sentbtn.dom.enabled = true;
}
else
{
appendline(group, _("InstantMessage.SessionCreateFailed"));
}
}
});
// Handle incoming IMs.
AjaxLife.Network.MessageQueue.RegisterCallback('InstantMessage',function(data) {
// Ensure it's something to display
if(data.IMSessionID == AjaxLife.Utils.UUID.Zero) return; // Estate messages have null sessions.
if(data.Dialog == AjaxLife.Constants.MainAvatar.InstantMessageDialog.MessageFromAgent || data.Dialog == AjaxLife.Constants.MainAvatar.InstantMessageDialog.SessionSend)
{
// Create a tab for them if we haven't already. Also play new IM sound.
if(!chats[data.IMSessionID])
{
AjaxLife.Widgets.Ext.msg("",_("InstantMessage.NewIMSession", {from: data.FromAgentName}), "newimsession", true);
if(data.GroupIM) joingroupchat(data.IMSessionID);
var created = createTab(data.FromAgentID, data.FromAgentName, data.IMSessionID, data.GroupIM);
if(!created)
{
AjaxLife.Widgets.Ext.msg("Lost Instant Message","From: {0}
Message: {1}",data.FromAgentName,data.Message);
return;
}
if(!dialog.isVisible())
{
dialog.show();
}
Sound.play(AjaxLife.STATIC_ROOT+"sounds/im.wav");
}
// Format the incoming message, taking care of /me.
var message = data.Message;
if(message.substr(0,3) == "/me")
{
message = data.FromAgentName+message.substr(3);
}
else
{
message = data.FromAgentName+": "+message;
}
// Assume they stopped typing.
if(chats[data.IMSessionID].div_typing.dom.parentNode)
{
chats[data.IMSessionID].div_typing.dom.parentNode.removeChild(chats[data.IMSessionID].div_typing.dom);
}
// Actually add the line.
appendline(data.IMSessionID, message, {name: data.FromAgentName, id: data.FromAgentID});
// If the tab is not active, make it flash.
if(dialog.getTabs().getActiveTab().id != 'im-'+data.IMSessionID)
{
highlighttab(data.IMSessionID);
}
}
// If we have a tab for the sessionid...
if(chats[data.IMSessionID])
{
// Show typing note on StartTyping message, remove it on StopTyping.
if(data.Dialog == AjaxLife.Constants.MainAvatar.InstantMessageDialog.StartTyping)
{
// If it's already there higher up, remove it from there.
// This is arguably completely redundant.
if(chats[data.IMSessionID].div_typing.dom.parentNode)
{
chats[data.IMSessionID].div_typing.dom.parentNode.removeChild(chats[data.IMSessionID].div_typing.dom);
}
chats[data.IMSessionID].content.dom.appendChild(chats[data.IMSessionID].div_typing.dom);
// Scroll down to show it.
chats[data.IMSessionID].content.dom.scrollTop = chats[data.IMSessionID].content.dom.scrollHeight;
}
else if(data.Dialog == AjaxLife.Constants.MainAvatar.InstantMessageDialog.StopTyping)
{
if(chats[data.IMSessionID].div_typing &&
chats[data.IMSessionID].div_typing.dom.parentNode &&
typeof chats[data.IMSessionID].div_typing.dom.parentNode.removeChild == 'function')
{
chats[data.IMSessionID].div_typing.dom.parentNode.removeChild(chats[data.IMSessionID].div_typing.dom);
}
}
}
});
AjaxLife.Network.MessageQueue.RegisterCallback('CurrentGroups', fillgroups);
AjaxLife.Network.Send("RequestCurrentGroups",{});
// Highlighted tabs to flash every half second.
setInterval(processhighlight,500);
},
open: function(opener) {
if(opener)
{
dialog.show(opener);
}
else
{
dialog.show();
}
},
close: function() {
dialog.hide();
},
toggle: function(opener) {
if(!dialog.isVisible())
{
this.open(opener);
}
else
{
this.close();
}
},
start: function(id) {
AjaxLife.NameCache.Find(id, function(name) {
createTab(id,name,AjaxLife.Utils.UUID.Combine(gAgentID,id));
});
}
};
}();