Skip to content

Instantly share code, notes, and snippets.

@ryankinal
Created August 7, 2012 13:35
Show Gist options
  • Select an option

  • Save ryankinal/3285409 to your computer and use it in GitHub Desktop.

Select an option

Save ryankinal/3285409 to your computer and use it in GitHub Desktop.
My chat script (client side)
/**
Chat: A factory for multi-room chat clients.
methods:
create(container, rooms)
- container is the ID of the element in which to build the chat interface
- rooms is either an encrypted enrollment ID or an array of room definitions
- returns a ChatClient (described below)
**/
var Chat = (function() {
/**
ChatUser: Represents a user in the activeUsers list
in any given room. There may be multiple ChatUsers
that represent the same user, but in a different room.
members:
elem - the <li> element that contains user information
ignore - the anchor that allows the user to be (un)ignored
text - the user's name as a text node
id - the user's studentID
lastMessageSent - time since the user last sent a message
roomID - the room that the user sent the last message to
methods:
update(data)
- data is an object containing user data
- studentID
- username
- lastMessageSent - time since the last message the user sent (negative number)
- roomID - the room that the user sent the last message to
- ignored - whether the current user has ignored this user
- returns undefined
**/
var ChatUser = {
update: function(p_data)
{
if (!this.elem)
{
this.elem = document.createElement('li');
this.ignore = document.createElement('a');
this.text = document.createTextNode(p_data.username || 'guest');
/** stop ignoring yourself **/
/** stop ignoring yourself **/
if (!p_data.isSelf)
{
this.elem.appendChild(this.ignore);
}
this.elem.appendChild(this.text);
this.ignore.studentID = p_data.studentID;
this.ignoreText = document.createTextNode('block');
this.ignore.appendChild(this.ignoreText);
this.ignore.title = 'block this user';
this.ignore.className = 'block';
this.lastMessageSent = 0;
}
this.id = p_data.studentID;
this.lastMessageSent = p_data.lastMessageSent;
this.username = p_data.username;
this.roomID = p_data.roomID;
if (p_data.ignored)
{
this.ignoreText.nodeValue = 'unblock';
this.ignore.title = 'unblock this user';
this.ignore.className = 'unblock';
}
else
{
this.ignoreText.nodeValue = 'block';
this.ignore.title = 'block this user';
this.ignore.className = 'block';
}
if (this.roomID && (this.lastMessageSent || this.lastMessageSent === 0) && this.lastMessageSent > -30)
{
this.elem.className = 'chat-user-active';
}
else
{
this.elem.className = 'chat-user-inactive';
}
}
};
var ChatRoom = {
/**
handleClicks returns a function to be used as an event handler on the
room's container element. That function then handles all clicks within
that element, checking on classNames to determine how to handle the click.
**/
handleClicks: function(room)
{
// Create callback creates a callback to be used with ChatService webmethods
var createCallback = function(username, message, failureCallback)
{
failureCallback = failureCallback || function() {};
return function(p_data)
{
if (p_data)
{
room.addMessage({
username: username,
body: message
});
room.commitMessages();
}
else
{
failureCallback();
}
}
},
ignoreFailure = createCallback('system', 'There was a problem blocking the user. Please try again.'),
ignoreSuccess = createCallback('system', 'User blocked.', ignoreFailure),
unignoreFailure = createCallback('system', 'There was a problem unblocking the user. Please try again.'),
unignoreSuccess = createCallback('system', 'User unblocked.', unignoreFailure),
flagFailure = createCallback('system', 'There was a problem flagging the message. Please try again.'),
flagSuccess = createCallback('system', 'Message flagged.', flagFailure);
return function(p_e)
{
p_e = p_e || window.event;
var target = p_e.target || p_e.srcElement,
id = target.id;
switch (target.className)
{
case 'block':
ChatService.ignoreUser(
target.studentID,
ignoreSuccess,
ignoreFailure
);
target.firstChild.nodeValue = 'unblock';
target.className = 'unblock';
target.title = 'unblock this user';
break;
case 'unblock':
ChatService.ignoreUser(
target.studentID,
unignoreSuccess,
unignoreFailure
);
target.firstChild.nodeValue = 'block';
target.className = 'block';
target.title = 'block this user';
break;
case 'flag':
ChatService.flagMessage(
target.messageID,
flagSuccess,
flagFailure
);
target.className = 'flagged';
break;
case 'active-users-help':
$('#active-users-help').show().siblings().hide().end().parent().data('open')();
break;
}
}
},
show: function()
{
this.elem.style.display = 'block';
},
hide: function()
{
this.elem.style.display = 'none';
},
/**
addMessage creates a DocumentFragment if none exists, builds a message
element, and adds that element to the document fragment. In order
for the documentfragment to be appended to the document, commitMessages()
must be called.
**/
addMessage: function(p_message)
{
this.fragment = this.fragment || document.createDocumentFragment();
var elem = document.createElement('div'),
username = document.createElement('div'),
message = document.createElement('div'),
flag = document.createElement('a'),
body = p_message.body;
flag.title = 'flag this message as inappropriate';
flag.className = 'flag';
flag.messageID = p_message.id;
body = body.replace(p_message.username, '{name}');
body = body.replace('{name}', p_message.username);
if (p_message.username !== 'system' && !p_message.isSelf)
{
message.appendChild(flag);
}
message.appendChild(document.createTextNode(body));
message.className = 'body';
username.appendChild(document.createTextNode(p_message.username || 'guest'));
username.className = 'username';
elem.className = 'message';
elem.appendChild(username);
elem.appendChild(message);
if (p_message.username === 'system')
{
elem.className += ' system';
}
if (p_message.lastMessageSent)
{
this.lastMessageSent = new Date(p_message.lastMessageSent);
}
else
{
delete this.lastMessageSent;
}
this.fragment.appendChild(elem);
},
/**
"flushes the buffer" by appending the room's document
fragment to the document, if the fragment exists.
The fragment is then removed from the object.
**/
commitMessages: function()
{
var lms, str, year, pm;
if (this.fragment && this.fragment.childNodes.length > 0)
{
if (this.lastMessageSent)
{
year = this.lastMessageSent.getYear();
year = (year > 2000) ? year : year + 1900;
pm = (this.lastMessageSent.getHours() >= 12);
minutes = this.lastMessageSent.getMinutes();
minutes = (minutes < 10) ? '0' + minutes : minutes;
str = 'Last message sent on ' +
(this.lastMessageSent.getMonth() + 1) + '/' +
this.lastMessageSent.getDate() + '/' +
year + ' at ' +
(this.lastMessageSent.getHours() % 12 || 12) + ':' +
minutes +
(pm ? ' pm' : ' am') +
' (eastern) - showing one hour of messages';
lms = document.createElement('div');
lms.className = 'message last-sent';
lms.appendChild(document.createTextNode(str));
this.fragment.appendChild(lms);
delete this.lastMessageSent;
}
else if (this.tab.className.indexOf('new') < 0 && this.tab.className.indexOf('active') < 0)
{
this.tab.className += ' new';
}
this.messages.insertBefore(this.fragment, this.bottom);
if (!this.scrolling)
{
$(this.messages).scrollTo(this.bottom);
}
delete this.fragment;
}
},
scrollToBottom: function()
{
$(this.messages).scrollTo(this.bottom);
this.scrolling = false;
},
/**
removes all users from the active users list
**/
clearUsers: function()
{
var users = this.activeUsers;
while (users.childNodes.length)
{
users.removeChild(users.firstChild);
}
},
/**
If the given user does not exist in the element list,
it is created. It is then updated and re-appended.
**/
addUser: function(p_user)
{
if (!this.users)
{
this.users = {};
}
if (!this.users[p_user.studentID])
{
this.users[p_user.studentID] = Object.create(ChatUser);
}
this.users[p_user.studentID].update(p_user);
this.activeUsers.appendChild(this.users[p_user.studentID].elem);
},
/**
Builds all necessary members and elements for this room
**/
initialize: function(p_room)
{
var element = document.createElement('div'),
activeUsers = document.createElement('div'),
usersHeader = document.createElement('h4'),
usersList = document.createElement('ul'),
messages = document.createElement('div'),
bottom = document.createElement('div'),
newMessage = document.createElement('span'),
tab = document.createElement('li');
element.className = 'room';
element.id = 'room' + p_room.id;
messages.className = 'messages';
bottom.className = 'bottom';
bottom.id = 'bottom' + p_room.id;
usersHeader.appendChild(document.createTextNode('Active users'));
usersHeader.className = 'active-users-help';
activeUsers.className = 'chat-active-users';
activeUsers.appendChild(usersHeader);
activeUsers.appendChild(usersList);
messages.appendChild(bottom);
element.appendChild(activeUsers);
element.appendChild(messages);
newMessage.title = 'There is a new message';
newMessage.appendChild(document.createTextNode('*'));
tab.appendChild(newMessage);
tab.appendChild(document.createTextNode(p_room.name));
tab.className = 'chat-tab';
tab.id = 'chat-tab' + p_room.id;
tab.title = p_room.description;
this.id = p_room.id;
this.elem = element;
this.tab = tab;
this.elem.onclick = this.handleClicks(this);
this.description = p_room.description;
this.name = p_room.name;
this.bottom = bottom;
this.activeUsers = usersList;
this.messages = messages;
this.scrolling = false;
this.messages.onscroll = (function(self) {
return function(p_e) {
if (self.messages.scrollTop < self.messages.scrollHeight - self.messages.offsetHeight)
{
self.scrolling = true
}
else
{
self.scrolling = false;
}
}
})(this);
this.hide();
}
};
var ChatClient = {
/**
If passed a string as the first argument, gets a list of
rooms related to that enrollment ID from the server,
and re-calls this method. With an array of objects
representing rooms.
If passed an array as the first argument, it creates a
new ChatRoom for each element, and initializes it as
necessary.
**/
setRooms: function(p_arg, self)
{
var i, element, tab, bottom, activeUsers, usersList, room, messages, fragment, newMesage, roomIDs = [];
self = self || this;
if (p_arg)
{
if (typeof p_arg === 'object' && p_arg.length)
{
fragment = document.createDocumentFragment();
self.rooms = {};
for (i = 0; i < p_arg.length; i++)
{
roomIDs.push(p_arg[i].id);
room = Object.create(ChatRoom);
room.initialize(p_arg[i]);
self.rooms[p_arg[i].id] = room;
self.tabs.appendChild(room.tab);
fragment.appendChild(room.elem);
}
self.output.appendChild(fragment);
self.activeRoom = self.rooms[p_arg[i - 1].id];
self.activeRoom.show();
self.tabs.lastChild.className += ' active';
ChatService.initialize(
roomIDs.join(','),
self.messagesSuccess(self),
self.messagesFail(self)
);
self.getActiveUsers(self);
}
else
{
ChatService.getRoomsByEnrollmentID(p_arg, function(data) { self.setRooms(data, self); });
}
}
},
messagesSuccess: function(context)
{
var s = [],
self = context || this;
return function(p_data)
{
var i, j, room, l, data, leetFilter = {'l': '1', 'e': '3', 't': '7', 'a': '4', 'i': '1', 'o': '0'}, rex, teh = /the/gi, cks = /(cks|ck)/gi;
if (p_data && p_data.length)
{
l = p_data.length;
for (i = 0; i < l; ++i)
{
data = p_data[i];
if (self.leet)
{
data.body = data.body.replace(teh, 'teh');
data.body = data.body.replace(cks, 'x');
for (j in leetFilter)
{
rex = new RegExp(j, 'gi');
data.body = data.body.replace(rex, leetFilter[j]);
}
}
self.rooms[data.roomID].addMessage(data);
}
self.commitMessages();
self.pollInterval = 1000;
self.activeRoom.tab.className = 'chat-tab active';
}
else
{
// If there is no data, we want to increase the poll interval,
// so we don't tax the server too severely. Five polls at the
// same interval means an increase of 1 second in the interval
if (++(self.counter) > 5)
{
self.pollInterval += (self.pollInterval < 10000) ? 1000 : 0;
self.counter = 0;
}
}
if (self.polling)
{
// make sure the timeout is cleared so
// we're not doubling up on the polling
if (self.messageTimeout)
{
clearTimeout(self.messageTimeout);
}
self.messageTimeout = setTimeout(self.getMessages(self), self.pollInterval);
}
};
},
messagesFail: function(context)
{
var self = context || this;
return function()
{
// Failure could mean a lot of things, but in any
// case, we don't want to tax the server with useless
// poll requests, so we increase the interval every
// time this happens, but set the counter to 0
if (self.messageTimeout)
{
clearTimeout(self.messageTimeout);
}
self.pollInterval += (self.pollInterval < 10000) ? 1000 : 0;
self.messageTimeout = setTimeout(self.getMessages(self), self.pollInterval);
self.counter = 0;
}
},
scrollAllToBottom: function()
{
var i;
for (i in this.rooms)
{
if (this.rooms.hasOwnProperty(i))
{
this.rooms[i].scrollToBottom();
}
}
},
/**
Gets messages for all rooms, and passes those messages
off to each individual room to be buffered as necessary.
Once all messages are processed, commitMessages is called,
which flushes the buffer of each room, thus displaying the messages.
**/
getMessages: function(context)
{
var s = [],
self = context || this;
return function()
{
var i; // join all room IDs into a string to be passed as an argument
for (i in this.rooms)
{
if (this.rooms.hasOwnProperty(i))
{
s.push(i);
}
}
ChatService.getMessages(
s.join(','),
self.messagesSuccess(self),
self.messagesFail(self)
);
}
},
// Flush all output buffers in all rooms
commitMessages: function()
{
var i, rooms = this.rooms;
for (i in rooms)
{
if (rooms.hasOwnProperty(i))
{
rooms[i].commitMessages();
}
}
},
// Gets all active users for all rooms every 10 seconds
// if a user is active (has sent polls), but has not
// sent any messages, then roomID will be null, and
// the user should be added to all rooms
getActiveUsers: function(context)
{
var s = [],
self = context || this;
return function()
{
var i;
for (i in self.rooms)
{
if (self.rooms.hasOwnProperty(i))
{
s.push(i);
}
}
ChatService.getActiveUsers(
s.join(','),
function(p_data)
{
var i, room, l, data;
if (p_data && p_data.length)
{
self.clearAllUsers();
l = p_data.length;
for (i = 0; i < l; ++i)
{
data = p_data[i];
if (p_data[i].roomID)
{
self.rooms[data.roomID].addUser(data);
}
else
{
self.addUserToAllRooms(data);
}
}
}
if (self.polling)
{
self.usersTimeout = setTimeout(self.getActiveUsers(self), 10000);
}
},
function()
{
self.usersTimeout = setTimeout(self.getActiveUsers(self), 10000);
}
);
}
},
clearAllUsers: function()
{
var i;
for (i in this.rooms)
{
if (this.rooms.hasOwnProperty(i))
{
this.rooms[i].clearUsers();
}
}
},
addUserToAllRooms: function(p_user)
{
var i;
for (i in this.rooms)
{
if (this.rooms.hasOwnProperty(i))
{
this.rooms[i].addUser(p_user);
}
}
},
/**
start polling for messages and users
**/
startPoll: function()
{
var self = this;
this.stopPoll();
this.polling = true;
this.getMessages(this)();
this.getActiveUsers(this)();
setTimeout(function() {
$(self.activeRoom.messages).scrollTo(self.activeRoom.bottom);
}, platform.animationSpeed / 2);
},
/**
stop all polling for messages and users
**/
stopPoll: function()
{
if (this.messageTimeout)
{
clearTimeout(this.messageTimeout)
}
if (this.usersTimeout)
{
clearTimeout(this.usersTimeout);
}
this.polling = false;
this.pollInterval = 1000;
},
/**
sets which room is the active room
This is mostly called from the click handler
**/
setActiveRoom: function(p_id)
{
if (this.rooms[p_id])
{
var i;
this.activeRoom = this.rooms[p_id];
for (i in this.rooms)
{
if (this.rooms.hasOwnProperty(i))
{
this.rooms[i].hide();
}
}
this.activeRoom.show();
this.activeRoom.tab.className = 'chat-tab active';
if (!this.scrolling)
{
$(this.activeRoom.messages).scrollTo(this.activeRoom.bottom);
}
}
},
/**
Sends a message to the active room
**/
sendMessage: function()
{
var message = this.input.value,
self = this;
if (message && message !== '')
{
if (message === '/1337')
{
if (this.leet)
{
this.leet = false;
this.activeRoom.addMessage({
'username': 'system',
'body': 'leet hacks disabled'
});
this.activeRoom.commitMessages();
}
else
{
this.leet = true;
this.activeRoom.addMessage({
'username': 'system',
'body': '1337 h4x 3n4bl3d'
});
this.activeRoom.commitMessages();
}
this.input.value = '';
return false;
}
this.input.value = '';
platform.inactivity.update();
ChatService.sendMessage(
self.activeRoom.id,
message,
function(p_data)
{
// Since we know there's data,
// we might as well reset the poll timer
// and go get it
if (this.messageTimeout)
{
clearTimeout(self.messageTimeout);
}
self.pollInterval = 0;
self.getMessages(self)();
self.input.value = '';
},
function()
{
self.error('There was a problem sending your message. Please try again.');
self.input.value = message;
}
);
}
},
error: function(p_message)
{
self.activeRoom.addMessage({
username: 'system',
body: p_message
});
},
/**
Returns a function which is used as a click handler.
This click handler handles all clicks for the entire
chat client via delegation.
**/
handleClicks: function(chat)
{
return function(p_e)
{
p_e = p_e || window.event;
var target = p_e.target || p_e.srcElement,
className = target.className,
id = target.id.replace('chat-tab', ''),
tabs,
i;
if (platform && platform.inactivity)
{
platform.inactivity.update();
}
switch (target.className)
{
case 'chat-send':
chat.sendMessage();
break;
case 'chat-rooms-help':
$('#chat-rooms-help').show().siblings().hide().end().parent().data('open')();
break;
case 'chat-tab':
case 'chat-tab new':
chat.setActiveRoom(id);
tabs = target.parentNode.getElementsByTagName('li');
for (i = 0; i < tabs.length; i++)
{
tabs[i].className = tabs[i].className.replace(/active/g, '').replace(/(^\s+|\s+$)/g, '').replace(/(\s{2,})/g, ' ');
}
target.className += ' active';
break;
}
}
},
/**
Set all necessary values for the client.
Sets up event handlers for the client.
**/
initialize: function(p_container, p_rooms)
{
var self = this,
roomsLabel = document.createElement('li');
roomsLabel.appendChild(document.createTextNode('Chat rooms:'));
roomsLabel.className = 'chat-rooms-help';
this.counter = 0;
this.container = document.getElementById(p_container);
if (!this.container)
{
return false;
}
this.container.onclick = this.handleClicks(self);
this.input = document.createElement('input');
this.input.type = 'text';
this.input.className = 'chat-input';
this.input.onkeypress = function(p_e)
{
p_e = p_e || window.event;
var which = p_e.which || p_e.keyCode;
if (which === 13)
{
self.sendMessage();
return false;
}
}
this.send = document.createElement('input');
this.send.type = 'button';
this.send.value = 'Send';
this.send.className = 'chat-send';
this.notifications = document.createElement('div');
this.notifications.className = 'chat-notifications';
this.tabs = document.createElement('ul');
this.tabs.className = 'chat-tabs header';
this.controls = document.createElement('div');
this.controls.className = 'chat-controls';
this.output = document.createElement('div');
this.output.className = 'chat-output';
this.tabs.appendChild(roomsLabel);
this.setRooms(p_rooms);
this.controls.appendChild(this.input);
this.controls.appendChild(this.send);
this.controls.appendChild(this.notifications);
this.container.appendChild(this.tabs);
this.container.appendChild(this.output);
this.container.appendChild(this.controls);
}
};
// This return defines the Factory interface
// Since "create" returns a ChatClient, all
// methods of ChatClient are availabe to the
// returned object
return {
create: function(p_container, p_rooms, p_init)
{
var client = Object.create(ChatClient);
if (p_init || p_init === 'undefined')
{
client.initialize(p_container, p_rooms);
}
return client;
}
};
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment