User:Jonas Lund/tri1.2
Let's Talk About Pictures (LTAP)
The LTAP is a collaboration with Louis Doulas and Harry Burke, to create a website that facilitates and encourages a one to one discussion about pictures. In LTAP, each user is initially presented with an upload form which accepts images. The user is required to upload a picture of their choosing and pick a nickname to enter the discussion, the chat room. The chat room displays a random picture from the database of images and the conversation can begin.
LTAP Development
Plan
Use nodejs, with the modules nowjs, mysql, forever, formidable, to create a realtime chat with image uploads and a mysql database for storage. (Consider switching mysql for couchdb or mongodb).
Resources
Application Logic
- Nodejs http://nodejs.org/
- Nowjs for handling socket.io http://nowjs.com/
- Express, a base app template systemconsider switching for next version. http://expressjs.com/guide.html
- Debugging with node-inspector http://elegantcode.com/2011/01/14/taking-baby-steps-with-node-js-debugging-with-node-inspector/
File Uploads
- Node Formidable Module for handling multipart uploadshttps://github.com/felixge/node-formidable/
- http://stackoverflow.com/questions/5149545/uploading-images-using-nodejs-express-and-mongo
- http://debuggable.com/posts/streaming-file-uploads-with-node-js:4ac094b2-b6c8-4a7f-bd07-28accbdd56cb
Mysql, Mysql Module
General articles to read
- http://www.debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb
- http://www.martinfowler.com/articles/injection.html
- http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html
- Simple beginner application stack http://www.nodebeginner.org/#the-application-stack
Deployment
- Look into Nginx
- Http-proxy https://github.com/nodejitsu/node-http-proxy
- Forever (node based CLI for starting/stopping node servers in the background) https://github.com/nodejitsu/forever/
- shell-script for stopping/starting server.js – replaced by Forever http://gitorious.org/naked-on-pluto/game-server/blobs/master/server.sh
Source
server.js
//db
var mysql = require('mysql');
var db = "ltappiting";
var chats = 'chats';
var chatLogs = 'chat_logs';
var client = mysql.createClient({
host: "localhost",
user: "root",
password: "root"
});
//server
var http = require("http");
var url = require("url");
var fs = require("fs");
var util = require('util');
var formidable = require('formidable');
var exec = require('child_process').exec;
var fu = exports;
function onRequest(req, res) {
var pathname = url.parse(req.url).pathname;
console.log("Request for " + pathname + " received.");
//routing
switch (pathname) {
case '/':
req.setEncoding("utf8");
var html = fs.readFileSync(__dirname+'/index.html');
res.end(html);
break;
case '/upload':
uploadFile(req, res);
break;
default:
req.setEncoding("utf8");
var content_type = fu.mime.lookupExtension(extname(pathname));
fs.readFile(__dirname+"/"+pathname, function(err, data) {
if(err) {
res.writeHead(404, {"Content-Type": "text/plain"});
res.write("404 Not found");
res.end();
} else {
headers = { "Content-Type": content_type
, "Content-Length": data.length
};
res.writeHead(200, headers);
res.write(data);
res.end();
}
});
break;
}
}
var server = http.createServer(onRequest).listen(1337);
console.log("server running on 1337");
var nowjs = require("now");
var everyone = nowjs.initialize(server);
//Session Variables
var sessions = {};
var rooms = [];
rooms[0] = 0;
// I'm working on a 1-1 chat client with nodejs and nowjs, and I'm wondering about the best practice for queuing/assigning rooms (groups) to the users, given that it should only be a 1 to 1 conversation, and the one who's been waiting the longest should be assigned first'. Any ideas? :)
//Send message to everyone on the users group
everyone.now.distributeMessage = function(message){
console.log('Received '+message+' from '+this.now.nick +' in serverroom '+this.now.room);
var group = nowjs.getGroup(this.now.room);
group.now.receiveMessage(this.now.id, this.now.nick, message);
var date = new Date();
//insert into DB
client.query('USE '+db);
client.query(
'INSERT INTO '+chatLogs+' '+
'SET user = ?, text = ?, date = ?',
[this.user.clientId, message, date]
);
};
//function to be called from client
everyone.now.join = function() {
var userID = this.user.clientId;
var nick = this.now.nick;
var image = this.now.image;
var id = this.now.id;
//get available room
var room = getRoom();
this.now.room = room;
var user = {
id: userID,
nick: nick,
room: room,
image: image
//timestamp: new Date()
};
//add user to room
nowjs.getGroup(room).addUser(userID);
//add user to session
sessions[user.id] = user;
var group = nowjs.getGroup(this.now.room);
if(rooms[room] == 2) {
var status = "Buddy Joined, say Hi!";
//get-users, add to db
nowjs.getGroup(room).getUsers(function (users) {
var user1 = users[0];
var user2 = users[1];
var nick1 = sessions[user1].nick;
var image1 = sessions[user1].image;
var nick2 = sessions[user2].nick;
var image2 = sessions[user2].image;
var date = new Date();
//var randNumber = Math.floor(Math.random()*11) + 11;
client.query('USE '+db);
client.query(
//'SELECT image1, id FROM '+chats + ' WHERE id=' + randNumber,
'SELECT image1, id FROM '+chats + ' ORDER BY rand() LIMIT 1',
function selectCb(err, results, fields) {
if (err) {
throw err;
}
var chatImage = results[0].image1;
client.end();
console.log(chatImage);
group.now.updateImage(chatImage);
//insert into DB
client.query('USE '+db);
client.query(
'INSERT INTO '+chats+' '+
'SET user1 = ?, user2 = ?, nick1 = ?, nick2 = ?, image1 = ?, image2 = ?, chatimage = ?, date = ?',
[user1, user2, nick1, nick2, image1, image2, chatImage, date]
);
});
});
} else {
var status = "Finding chat buddy..";
}
group.now.chatStatus(status);
}
everyone.now.isTyping = function(type){
console.log('Received '+type+' from '+this.now.id);
var group = nowjs.getGroup(this.now.room);
group.now.updateType(type, this.now.nick, this.now.id);
};
nowjs.on('disconnect', function () {
var userroom = this.now.room;
var nick = this.now.nick;
var id = this.now.id;
var userID = this.user.clientId;
if(rooms[userroom] == 2) {
rooms[userroom] = 1;
var status = "Buddy Left.. – Looking for a new one...";
} else {
rooms[userroom] = 0;
}
if (userID && sessions[id]) {
delete sessions[userID];
}
var group = nowjs.getGroup(this.now.room);
group.now.chatStatus(status);
console.log("user:", this.now.id, this.now.nick, " left");
});
function getRoom() {
for (var i=0; i < rooms.length; i++) {
if(rooms[i] === 1) {
rooms[i] = 2;
return i;
} else if (rooms[i] === 0) {
rooms[i] = 1;
return i;
}
};
rooms.push(1);
return rooms.length-1;
}
//upload
function uploadFile(req, res) {
var form = new formidable.IncomingForm(),
files = [],
fields = [];
fileob = {};
form.uploadDir = __dirname + '/tmp';
form.encoding = 'binary';
form
.on('field', function(field, value) {
console.log(field, value);
fields.push([field, value]);
})
.on('file', function(field, file) {
files.push([field, file]);
//check size type, cancel if wrong
var newfile = {
file: file.size,
name: file.name,
path: file.path,
type: file.type
};
fileob = newfile;
})
.on('end', function() {
console.log('-> upload done');
res.writeHead(200, {'content-type': 'text/plain'});
//move
var ext = extname(fileob.name);
var uni = Math.floor(Math.random()*99999).toString();
var cleanFilename = fileob.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
var filename = uni + cleanFilename + ext;
res.write(filename);
res.end();
var cmd = "mv " + fileob.path + " /home/jonasx/t2sp.net/upload/" + filename;
console.log(cmd);
exec(cmd, puts);
});
form.parse(req, function(err, fields, files) {
if (err) {
console.log(err);
}
});
}
function puts(error, stdout, stderr) { util.puts(stdout) }
function include(arr,obj) {
return (arr.indexOf(obj) != -1);
}
Array.prototype.remove = function(from, to) {
var rest = this.slice((to || from) + 1 || this.length);
this.length = from < 0 ? this.length + from : from;
return this.push.apply(this, rest);
};
function extname (path) {
var index = path.lastIndexOf(".");
return index < 0 ? "" : path.substring(index);
}
// stolen from node_chat, jack- thanks
fu.mime = {
// returns MIME type for extension, or fallback, or octet-steam
lookupExtension : function(ext, fallback) {
return fu.mime.TYPES[ext.toLowerCase()] || fallback || 'application/octet-stream';
},
// List of most common mime-types, stolen from Rack.
TYPES : { ".3gp" : "video/3gpp"
, ".a" : "application/octet-stream"
, ".ai" : "application/postscript"
, ".aif" : "audio/x-aiff"
, ".aiff" : "audio/x-aiff"
, ".asc" : "application/pgp-signature"
, ".asf" : "video/x-ms-asf"
, ".asm" : "text/x-asm"
, ".asx" : "video/x-ms-asf"
, ".atom" : "application/atom+xml"
, ".au" : "audio/basic"
, ".avi" : "video/x-msvideo"
, ".bat" : "application/x-msdownload"
, ".bin" : "application/octet-stream"
, ".bmp" : "image/bmp"
, ".bz2" : "application/x-bzip2"
, ".c" : "text/x-c"
, ".cab" : "application/vnd.ms-cab-compressed"
, ".cc" : "text/x-c"
, ".chm" : "application/vnd.ms-htmlhelp"
, ".class" : "application/octet-stream"
, ".com" : "application/x-msdownload"
, ".conf" : "text/plain"
, ".cpp" : "text/x-c"
, ".crt" : "application/x-x509-ca-cert"
, ".css" : "text/css"
, ".csv" : "text/csv"
, ".cxx" : "text/x-c"
, ".deb" : "application/x-debian-package"
, ".der" : "application/x-x509-ca-cert"
, ".diff" : "text/x-diff"
, ".djv" : "image/vnd.djvu"
, ".djvu" : "image/vnd.djvu"
, ".dll" : "application/x-msdownload"
, ".dmg" : "application/octet-stream"
, ".doc" : "application/msword"
, ".dot" : "application/msword"
, ".dtd" : "application/xml-dtd"
, ".dvi" : "application/x-dvi"
, ".ear" : "application/java-archive"
, ".eml" : "message/rfc822"
, ".eps" : "application/postscript"
, ".exe" : "application/x-msdownload"
, ".f" : "text/x-fortran"
, ".f77" : "text/x-fortran"
, ".f90" : "text/x-fortran"
, ".flv" : "video/x-flv"
, ".for" : "text/x-fortran"
, ".gem" : "application/octet-stream"
, ".gemspec" : "text/x-script.ruby"
, ".gif" : "image/gif"
, ".gz" : "application/x-gzip"
, ".h" : "text/x-c"
, ".hh" : "text/x-c"
, ".htm" : "text/html"
, ".html" : "text/html"
, ".ico" : "image/vnd.microsoft.icon"
, ".ics" : "text/calendar"
, ".ifb" : "text/calendar"
, ".iso" : "application/octet-stream"
, ".jar" : "application/java-archive"
, ".java" : "text/x-java-source"
, ".jnlp" : "application/x-java-jnlp-file"
, ".jpeg" : "image/jpeg"
, ".jpg" : "image/jpeg"
, ".js" : "application/javascript"
, ".json" : "application/json"
, ".log" : "text/plain"
, ".m3u" : "audio/x-mpegurl"
, ".m4v" : "video/mp4"
, ".man" : "text/troff"
, ".mathml" : "application/mathml+xml"
, ".mbox" : "application/mbox"
, ".mdoc" : "text/troff"
, ".me" : "text/troff"
, ".mid" : "audio/midi"
, ".midi" : "audio/midi"
, ".mime" : "message/rfc822"
, ".mml" : "application/mathml+xml"
, ".mng" : "video/x-mng"
, ".mov" : "video/quicktime"
, ".mp3" : "audio/mpeg"
, ".mp4" : "video/mp4"
, ".mp4v" : "video/mp4"
, ".mpeg" : "video/mpeg"
, ".mpg" : "video/mpeg"
, ".ms" : "text/troff"
, ".msi" : "application/x-msdownload"
, ".odp" : "application/vnd.oasis.opendocument.presentation"
, ".ods" : "application/vnd.oasis.opendocument.spreadsheet"
, ".odt" : "application/vnd.oasis.opendocument.text"
, ".ogg" : "application/ogg"
, ".p" : "text/x-pascal"
, ".pas" : "text/x-pascal"
, ".pbm" : "image/x-portable-bitmap"
, ".pdf" : "application/pdf"
, ".pem" : "application/x-x509-ca-cert"
, ".pgm" : "image/x-portable-graymap"
, ".pgp" : "application/pgp-encrypted"
, ".pkg" : "application/octet-stream"
, ".pl" : "text/x-script.perl"
, ".pm" : "text/x-script.perl-module"
, ".png" : "image/png"
, ".pnm" : "image/x-portable-anymap"
, ".ppm" : "image/x-portable-pixmap"
, ".pps" : "application/vnd.ms-powerpoint"
, ".ppt" : "application/vnd.ms-powerpoint"
, ".ps" : "application/postscript"
, ".psd" : "image/vnd.adobe.photoshop"
, ".py" : "text/x-script.python"
, ".qt" : "video/quicktime"
, ".ra" : "audio/x-pn-realaudio"
, ".rake" : "text/x-script.ruby"
, ".ram" : "audio/x-pn-realaudio"
, ".rar" : "application/x-rar-compressed"
, ".rb" : "text/x-script.ruby"
, ".rdf" : "application/rdf+xml"
, ".roff" : "text/troff"
, ".rpm" : "application/x-redhat-package-manager"
, ".rss" : "application/rss+xml"
, ".rtf" : "application/rtf"
, ".ru" : "text/x-script.ruby"
, ".s" : "text/x-asm"
, ".sgm" : "text/sgml"
, ".sgml" : "text/sgml"
, ".sh" : "application/x-sh"
, ".sig" : "application/pgp-signature"
, ".snd" : "audio/basic"
, ".so" : "application/octet-stream"
, ".svg" : "image/svg+xml"
, ".svgz" : "image/svg+xml"
, ".swf" : "application/x-shockwave-flash"
, ".t" : "text/troff"
, ".tar" : "application/x-tar"
, ".tbz" : "application/x-bzip-compressed-tar"
, ".tcl" : "application/x-tcl"
, ".tex" : "application/x-tex"
, ".texi" : "application/x-texinfo"
, ".texinfo" : "application/x-texinfo"
, ".text" : "text/plain"
, ".tif" : "image/tiff"
, ".tiff" : "image/tiff"
, ".torrent" : "application/x-bittorrent"
, ".tr" : "text/troff"
, ".txt" : "text/plain"
, ".vcf" : "text/x-vcard"
, ".vcs" : "text/x-vcalendar"
, ".vrml" : "model/vrml"
, ".war" : "application/java-archive"
, ".wav" : "audio/x-wav"
, ".wma" : "audio/x-ms-wma"
, ".wmv" : "video/x-ms-wmv"
, ".wmx" : "video/x-ms-wmx"
, ".wrl" : "model/vrml"
, ".wsdl" : "application/wsdl+xml"
, ".xbm" : "image/x-xbitmap"
, ".xhtml" : "application/xhtml+xml"
, ".xls" : "application/vnd.ms-excel"
, ".xml" : "application/xml"
, ".xpm" : "image/x-xpixmap"
, ".xsl" : "application/xml"
, ".xslt" : "application/xslt+xml"
, ".yaml" : "text/yaml"
, ".yml" : "text/yaml"
, ".zip" : "application/zip"
}
};
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>LTAP</title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="style.css">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="/nowjs/now.js"></script>
<script type="text/javascript">
util = {
urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g,
toStaticHTML: function(inputHtml) {
inputHtml = inputHtml.toString();
return inputHtml.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
},
zeroPad: function (digits, n) {
n = n.toString();
while (n.length < digits)
n = '0' + n;
return n;
},
timeString: function (date) {
var minutes = date.getMinutes().toString();
var hours = date.getHours().toString();
return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes);
},
isBlank: function(text) {
var blank = /^\s*$/;
return (text.match(blank) !== null);
}
};
$(document).ready(function(){
var CONFIG = { focus: true
,unread: 0
};
now.receiveMessage = function(id, nick, messageRaw){
if(id === now.id) {
var nick = "you";
}
if(!CONFIG.focus){
CONFIG.unread++;
updateTitle();
}
var message = util.toStaticHTML(messageRaw);
var time = new Date();
var relTime = util.timeString(time);
var lastMessage = $("#message div:last");
var lastMessageTime = $(lastMessage).find(".time").text();
if($(lastMessage).attr("data-id") === id) {
if(relTime === lastMessageTime) {
var content = '<span class="clear">'
// + ' <span class="time">' + relTime + '</span>'
+ ' <span class="msg-text">' + message + '</span>'
+ '</span>';
} else {
var content = '<span class="clear">'
+ ' <span class="time">' + relTime + '</span>'
+ ' <span class="msg-text">' + message + '</span>'
+ '</span>';
}
$(lastMessage).append(content);
$("#message").scrollTop(55000);
} else {
var content = '<div data-id="'+id+'">'
+ ' <span class="time">' + util.timeString(time) + '</span>'
+ ' <span class="nick '+nick+'">' + nick + '</span>'
+ ' <span class="msg-text">' + message + '</span'
+ '</div>'
;
$("#message").append(content).scrollTop(55000);
}
if($(".isTyping").is("visible")) {
$(".isTyping").appendTo($("#messages"));
}
}
$(".send").click(function() {
var input = "";
now.messageRoom(currRoom, input.text());
});
now.chatStatus = function(status) {
var time = new Date();
var content = '<div>'
+ ' <span class="time">' + util.timeString(time) + '</span>'
+ ' <span class="status">' + status + '</span>'
+ '</div>'
;
if(status === "Buddy Joined, say Hi!") {
$("#text-input").prop("disabled", false).focus();
}
if(status === "Buddy Left.. – Looking for a new one...") {
$("#text-input").prop("disabled", true);
}
$("#message").append(content).scrollTop(55000);
}
now.updateImage = function(image) {
$("#image").attr("src", image).show();
}
now.updateType = function(val, nick, id) {
console.log(val, nick, id, now.id);
if(id != now.id) {
if(val === true) {
$(".isTyping").text(nick + " is typing…").appendTo($("#message")).show();
$("#message").scrollTop(55000);
} else {
$(".isTyping").hide();
}
}
}
$("#chatBox").bind("keypress", function(e) {
var code = (e.keyCode ? e.keyCode : e.which);
if(code == 13) { //Enter keycode
var message = $("#text-input").val();
if(message) {
now.distributeMessage($("#text-input").val());
$("#text-input").val("");
}
now.isTyping(false);
return false;
}
now.isTyping(true);
});
$("#text-input").bind("blur", function() {
console.log("blurred");
now.isTyping(false);
});
$("#loginform").submit(function(){
var nick = $("#nick").val();
if(!nick) {
var nick = "Stranger";
}
var image = $("#imageurl").val();
if(!image) {
$(".error").html("Image Can't Be Empty").show();
} else {
//ugly regex hack for validating image
var match = image.match(/http:\/\/.+?\.jpg|jpeg|png|gif/gi);
if(!match) {
$(".error").html("Not a valid Image Url").show();
} else {
now.id = Math.floor(Math.random()*99999999999).toString();
now.room = 1;
now.nick = nick;
now.image = image;
now.join(nick, image);
$("#chat").show();
$("#login").hide();
$("#text-input").focus();
}
}
return false;
});
$("#file").submit(function() {
$("#transFrame").load(function() {
response = $(this).contents().find('body').text();
if(response) {
$("#file").hide();
var filename = "http://t2sp.net/upload/" + response;
$("#imageurl").val(filename);
$(".toggle-handle").hide();
$("#imageurl").prop("disabled", true);
}
//return false;
});
});
$(".toggle-handle").click(function() {
$(this).next(".toggle-box").show();
return false;
});
// on establishing 'now' connection, set server room and allow message sending
now.ready(function(){
});
//listen for browser events so we know to update the document title
$(window).bind("blur", function() {
CONFIG.focus = false;
updateTitle();
});
$(window).bind("focus", function() {
CONFIG.focus = true;
CONFIG.unread = 0;
updateTitle();
});
function updateTitle(){
if (CONFIG.unread) {
document.title = "(" + CONFIG.unread.toString() + ") LTAP";
} else {
document.title = "LTAP";
}
}
});
</script>
</head>
<body>
<div id="login">
<hgroup>
<h1>LTAP</h1>
</hgroup>
<form id="loginform" action="" method="get">
<h3 class="error hidden"></h3>
<input type="text" id="nick" placeholder="nickname (optional)"/>
<input type="text" id="imageurl" placeholder="Image (URL)"/>
<input type="submit" id="submit" value="go" class="button"/>
</form>
<h3 class='toggle-handle'>Or Upload A File...</h3>
<div class='toggle-box hidden'>
<form target="transFrame" id="file" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file-upload" id="file-upload" multiple="multiple">
<input type="submit" value="Upload" class="button">
</form>
</div>
</div>
<iframe style="" class="hidden" name="transFrame" id="transFrame"></iframe>
<div id="chat" class="hidden">
<div class="image">
<img id="image" class="" src="http://www.pandl.org/img/1x1" alt="placeholder img"/>
</div>
<div class="right">
<div id="message">
<span class="isTyping hidden"></span>
</div>
<form id="chatBox" action="" method="get">
<textarea type="text" id="text-input" name="chat" value="" disabled></textarea>
</form>
</div>
</div>
<div class="disclaimer">
<p>* all conversations are being stored in a database</p>
</div>
</body>
</html>
style.css
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
audio:not([controls]) { display: none; }
[hidden] { display: none; }
html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
html, button, input, select, textarea { font-family: sans-serif; color: #222; }
body { margin: 0; font-size: 1em; line-height: 1.4;}
::-moz-selection { background: #333; color: #fff; text-shadow: none; }
::selection { background: #333; color: #fff; text-shadow: none; }
/* =============================================================================
Links
========================================================================== */
a { color: #00e; }
a:visited { color: #551a8b; }
a:hover { color: #06e; }
a:focus { outline: thin dotted; }
a:hover, a:active { outline: 0; }
ul, li, ol { margin: 0; padding: 0; list-style: none;}
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
form { margin: 0; }
fieldset { border: 0; margin: 0; padding: 0; }
label { cursor: pointer; }
legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; }
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
button, input { line-height: normal; }
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
button[disabled], input[disabled] { cursor: default; }
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; }
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
textarea { overflow: auto; vertical-align: top; resize: vertical; }
html, body {width: 100%; height: 100%; font: 16px/1.3 sans-serif; overflow: hidden; margin: 0; padding: 0;}
#login input {margin-bottom: 10px; width: 320px; border: 0; border-bottom: 1px solid; font-size: 24px; color: #000;}
#login input[type='submit'] {width: 160px; margin-left: 80px; margin-bottom: 20px;}
#login {width: 320px; margin: 100px auto;}
.error {color: #f00;}
.image {width: 50%; float: left; text-align: center; padding-top: 10px; }
.image img {max-width: 80%; max-height: 80%;}
.right {bottom: 60px;left: 50%;position: absolute;right: 20px;top: 10px;}#chat {height: 100%; width: 100%; margin: 0; padding: 0;}
#message {height: 85%; width: 100%; border: 1px solid #ccc; padding: 10px 0 10px 10px; overflow-y: scroll; margin-bottom: 10px;}
#message div {padding-bottom: 5px;}
#chatBox {height: 15%;}
#text-input {width: 100%;height: 100%; margin: 0; padding: 10px 0 10px 10px; border: 1px solid #ccc; font-size: 16px;}
h3 {color: #333;}
.nick {color: red;}
.you {color: blue;}
.disclaimer {font-size: 12px; color: #666; position: fixed; bottom: 5px; left: 5px;}
.isTyping {font-size: 14px; font-weight: bold; padding-top: 10.4px;}
input:valid, textarea:valid { }
input:invalid, textarea:invalid { background-color: #f0dddd; }
.hidden { display: none;}
.invisible { visibility: hidden; }
.cf:before, .cf:after, #message div:before, #message div:after { content: ""; display: table; }
.cf:after, #message div:after { clear: both; }
.cf, #message div { *zoom: 1; }
.button {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background: white url('button.png') 0 0 repeat-x;
background: -moz-linear-gradient(0% 170% 90deg, #c4c4c4, white);
background: -webkit-gradient(linear, 0% 0%, 0% 170%, from(white), to(#c4c4c4));
border: 1px solid;
border-color: #e6e6e6 #cccccc #cccccc #e6e6e6;
border-radius: 3px;
color: #404040;
display: inline-block;
font-family: "helvetica neue", helvetica, arial, freesans, "liberation sans", "numbus sans l", sans-serif;
font-size: 13px;
outline 0;
padding: 5px 8px 5px;
text-align: center;
text-decoration: none;
text-shadow: 1px 1px 0 white;
white-space: nowrap; }
.button:hover {
background: -moz-linear-gradient(0% 170% 90deg, #b8b8b8, white);
background: -webkit-gradient(linear, 0% 0%, 0% 170%, from(white), to(#b8b8b8));
border-color: #99ccff;
color: #333333; }
.button:active {
position: relative;
top: 1px; }
.button:active, .button:focus {
background-position: 0 -25px;
background: -moz-linear-gradient(0% 170% 90deg, white, #dedede);
background: -webkit-gradient(linear, 0% 0%, 0% 170%, from(#dedede), to(white));
border-color: #8fc7ff #94c9ff #94c9ff #8fc7ff;
color: #1a1a1a;
text-shadow: 1px -1px 0 rgba(255, 255, 255, 0.5); }
.time {color: #666; font-size: 14px; float: right; margin-right: 10px;}
#message div {border-bottom: 1px dotted rgb(230,230,230); padding: 6px 0px 5px 0; clear: both;}
.nick {float: left; margin-right: 5px;}
.msg-text {float: left; width: 90%; clear: both;}
sql
CREATE TABLE `chat_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` tinytext,
`text` text,
`date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `chats` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user1` tinytext,
`user2` tinytext,
`nick1` tinytext,
`nick2` tinytext,
`image1` tinytext,
`image2` tinytext,
`chatimage` tinytext,
`date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;