<!doctype html> <html> <head> <meta charset="utf-8"> <title>tomsg webclient</title> <script> var PROTOCOL_VERSION=3; // Note: message id's are considered strings in this code. This is to not have to deal with full 64-bit integers. var sock=null,negotiated_version=false,username=null; var messagecache=new Map(); // msgid => [user,message] var roomlist=[":console"]; var currentroom=":console"; var currentreply=null; // [msgid, table row] or null var roomlogs=new Map([[":console",[]]]); // see drawRoomEntry for entry types var commandlist=[ {cmd:"register",usage:"/register <username> <password>"}, {cmd:"login",usage:"/login <username> <password>"}, {cmd:"createroom",usage:"/createroom"}, {cmd:"invite",usage:"/invite <username> (adds to current room)"}, {cmd:"leave",usage:"/leave (leaves the current room)"}, {cmd:"shrug",usage:"/shrug (sends '¯\\_(ツ)_/¯')"}, {cmd:"help",usage:"/help (show list of commands)"} ]; var uniqid=(function(){ var id=1; return function uniqid(){ return id++; }; })(); function now(){ return new Date().getTime(); } var net_callbacks={}; function net_send(msg,cb){ var id=uniqid()+""; sock.send(id+" "+msg); console.log("Sent '"+id+" "+msg+"'"); net_callbacks[id]=cb; } function fancySplit(text,freespace){ var obj={ word: [], rest: [], }; var cursor=0,idx; if(freespace){ while(cursor<text.length&&text[cursor]==" ")cursor++; } while(cursor<text.length){ idx=text.indexOf(" ",cursor); if(idx==-1)idx=text.length; obj.word.push(text.slice(cursor,idx)); obj.rest.push(text.slice(cursor)); if(idx==text.length)break; cursor=idx+1; if(freespace){ while(cursor<text.length&&text[cursor]==" ")cursor++; } else if(cursor==text.length){ obj.word.push(""); obj.rest.push(""); } } return obj; } function leftPad(str,num,chr){ str=str+""; if(str.length>=num)return str; var pad=""; while(pad.length<num-str.length)pad+=chr; return pad+str; } function formatTime(date){ return leftPad(date.getHours(),2,"0")+":"+ leftPad(date.getMinutes(),2,"0")+":"+ leftPad(date.getSeconds(),2,"0"); } function net_historyCollectionCallback(id,list,count,cb, item,err){ if(err){ cb(null,err); } else { list.push(item); if(list.length==count){ cb(list); } else { net_callbacks[id]=net_historyCollectionCallback.bind(this,id,list,count,cb); } } } function resetState() { if(sock)sock.close(); negotiated_version=false; username=null; messagecache.clear(); net_callbacks={}; cancelReply(); } function reconnect(){ resetState(); var url; if(location.hostname!="")url="wss://"+location.hostname; else url="ws://localhost"; sock=new WebSocket(url+":29546"); updateStatus(); updateRoomList(); sock.addEventListener("message",function(msg){ var spl=fancySplit(msg.data,false); console.log(msg.data); var id=spl.word[0],type=spl.word[1]; if(id=="_push"){ if(type=="message"){ var r=spl.word[2],u=spl.word[3],t=new Date(+spl.word[4]/1000); var msgid=spl.word[5],replyid=spl.word[6]; var text=spl.rest[7]; messagecache.set(msgid,[u,text]); addRoomEntry(r,"message",[u,t,msgid,replyid,text]); } else if(type=="invite"){ var r=spl.word[2],inviter=spl.word[3]; roomlist.push(r); roomlogs.set(r,[]); if(inviter==username)addRoomEntry(r,"notice",[now(),"You created this room in another session"]); else addRoomEntry(r,"notice",[now(),"User '"+inviter+"' invited you to this room"]); fetchRoomHistory(r); updateRoomList(); } else if(type=="join"){ var r=spl.word[2],u=spl.word[3]; addRoomEntry(r,"notice",[now(),"User '"+u+"' joined this room"]); } else if(type=="leave"){ var r=spl.word[2],u=spl.word[3]; if(u==username)uiLeaveRoom(r); else addRoomEntry(r,"notice",[now(),"User '"+u+"' left the room"]); } else if(type=="ping"){ // Do nothing } else if(type=="online"){ // TODO: implement nicklist and do something with this } else { alert("Unknown push message type '"+type+"'!"); } } else if(net_callbacks[id]){ var fn=net_callbacks[id]; delete net_callbacks[id]; var obj; if(type=="ok")fn(true); else if(type=="error")fn(null,spl.rest[2]); else if(type=="number")fn(spl.rest[2]); // Note: no int parse! else if(type=="name")fn(spl.rest[2]); else if(type=="list")fn(spl.word.slice(3)); else if(type=="history"){ var count=+spl.word[2]; net_callbacks[id]=net_historyCollectionCallback.bind(this,id,[],count,fn); } else if(type=="history_message")fn([spl.word[4],+spl.word[5]/1000,spl.word[6],spl.word[7],spl.rest[8]]); else if(type=="message")fn([spl.word[3],+spl.word[4]/1000,spl.word[5],spl.word[6],spl.rest[7]]); else alert("Unknown server response message type '"+type+"'!"); } else { alert("No callback for server message id '"+id+"'!"); } }); sock.addEventListener("open",function(){ updateStatus(); net_send("version "+PROTOCOL_VERSION,function(ok,err){ if(err){ var msg="Server version incompatible (we need protocol version "+ PROTOCOL_VERSION+"): "+err; addRoomEntry(":console","error",[now(),msg]); } else { negotiated_version=true; updateStatus(); } }); }); sock.addEventListener("close",function(ev){ updateStatus(); if(ev.code!=1000){ setTimeout(function(){ reconnect(); },1000+Math.random()*1000); } }); } function fetchRoomList(){ net_send("list_rooms",function(list,err){ if(err){ addRoomEntry(":console","error",[now(),"Unable to fetch room list: "+err]); } else { var roomid; for(roomid in roomlogs.keys()){ if(roomid!=":console")roomlogs.delete(roomid); } roomlist=[":console"]; var i; for(i=0;i<list.length;i++){ roomlist.push(list[i]); roomlogs.set(list[i],[]); fetchRoomHistory(list[i]); } updateRoomList(); } }); } function fetchRoomHistory(roomid){ net_send("history "+roomid+" 10",function(list,err){ if(err){ addRoomEntry(roomid,"error",[now(),"Unable to fetch history: "+err]); } else { var i; for(i=0;i<list.length;i++){ addRoomEntry(roomid,"message",list[i]); } } }); } function getMessage(msgid,callback){ if(messagecache.has(msgid)){ callback(null,messagecache.get(msgid)); return; } net_send("get_message "+msgid,function(item,err){ if(err)callback(err,null); else callback(null,item); }); } function updateStatus(){ var str=null; if(!sock||sock.readyState>=2){ str="not connected"; } else if(sock.readyState==0){ str="connecting..."; } else if(!negotiated_version){ str="versioning..."; } else if(username){ str="u: "+username; } else { str="connected"; } var div=document.getElementById("status"); if(!div.firstChild)div.appendChild(document.createTextNode("")); div.firstChild.nodeValue=str; } function updateRoomList(){ var rldiv=document.getElementById("roomlist"); var ch=rldiv.children,len=ch.length; var i; for(i=len-1;i>=0;i--)rldiv.removeChild(ch[i]); var div; for(i=0;i<roomlist.length;i++){ div=document.createElement("div"); div.classList.add("roomlistitem"); if(roomlist[i]==currentroom)div.classList.add("selected"); div.addEventListener("click",function(roomid){ currentroom=roomid; updateRoomList(); drawRoom(currentroom); document.getElementById("roominput").focus(); }.bind(this,roomlist[i])); div.appendChild(document.createTextNode(roomlist[i])); rldiv.appendChild(div); } } function uiLeaveRoom(roomid){ roomlist.splice(roomlist.indexOf(roomid),1); roomlogs.delete(roomid); var isCurrent=currentroom==roomid; if(isCurrent)currentroom=":console"; updateRoomList(); if(isCurrent)drawRoom(currentroom); } function drawRoom(roomid){ var tbody=document.getElementById("roomlog"); var ch=tbody.children,len=ch.length; var i; for(i=len-1;i>=0;i--)tbody.removeChild(ch[i]); var roomtitle=document.getElementById("roomtitle"); if(!roomtitle.firstChild)roomtitle.appendChild(document.createTextNode(roomid)); else roomtitle.firstChild.nodeValue=roomid; var tr,td; var items=roomlogs.get(roomid); if(!items){ alert("drawRoom on nonexistent roomid '"+roomid+"'!"); return; } for(i=Math.max(0,items.length-20);i<items.length;i++){ drawRoomEntry(items[i][0],items[i][1]); } } function drawRoomEntry(type,args){ var tbody=document.getElementById("roomlog"); var tr=document.createElement("tr"); var td0=document.createElement("td"); var td1=document.createElement("td"); var td2=document.createElement("td"); var node0=document.createTextNode(""); var node1=document.createTextNode(""); var node2=document.createTextNode(""); td0.appendChild(node0); td1.appendChild(node1); td2.appendChild(node2); tr.appendChild(td0); tr.appendChild(td1); tr.appendChild(td2); tbody.appendChild(tr); switch(type){ case "message": // [username, timestamp_ms, msgid, replyid, text] tr.classList.add("message"); node0.nodeValue=formatTime(new Date(args[1])); node1.nodeValue="<"+args[0]+">"; node2.nodeValue=args[4]; if(args[3]!="-1"){ var replyNode=document.createElement("span"); replyNode.classList.add("reply"); replyNode.classList.add("pending"); getMessage(args[3],function(err,item){ replyNode.classList.remove("pending"); var text; if(err){ replyNode.classList.add("failed"); text="Failed to fetch original message!"; } else { text="> <"+item[0]+"> "+item[4]; } replyNode.appendChild(document.createTextNode(text)); }); td2.insertBefore(replyNode,node2); td2.insertBefore(document.createElement("br"),node2); } tr.addEventListener("click",function(){ if(currentreply&¤treply[0]==args[2])cancelReply(); else startReply(args[2],tr); }); break; case "error": // [timestamp_ms, text] tr.classList.add("error"); node0.nodeValue=formatTime(new Date(args[0])); node1.nodeValue="--"; node2.nodeValue=args[1]; break; case "notice": // [timestamp_ms, text] tr.classList.add("notice"); node0.nodeValue=formatTime(new Date(args[0])); node1.nodeValue="--"; node2.nodeValue=args[1]; break; default: alert("drawRoomEntry on unknown type '"+type+"'!"); break; } tr.scrollIntoView(); } function addRoomEntry(roomid,type,args){ if(!roomlogs.get(roomid)){ alert("addRoomEntry on nonexistent roomid '"+roomid+"'!"); return; } roomlogs.get(roomid).push([type,args]); if(roomid==currentroom){ drawRoomEntry(type,args); } } function cancelReply(){ if(!currentreply)return; currentreply[1].classList.remove("replying"); document.getElementById("roominput_td").classList.remove("replying"); currentreply=null; } function startReply(msgid,tr){ cancelReply(); currentreply=[msgid,tr]; tr.classList.add("replying"); document.getElementById("roominput_td").classList.add("replying"); document.getElementById("roominput").focus(); } function showUsage(roomid,cmd){ for(var i=0;i<commandlist.length;i++){ if(commandlist[i].cmd==cmd){ addRoomEntry(roomid,"error",[now(),"Usage: "+commandlist[i].usage]); return; } } throw new Error("No usage registered for command '"+cnd+"'!"); } function executeCommand(roomid,text){ var spl=fancySplit(text,true); if(spl.word.length==0)return; var cmd=spl.word[0]; var creds=null,othername; switch(cmd){ case "register": case "login": if(spl.word.length<3){ showUsage(roomid,cmd); break; } creds=[spl.word[1],spl.rest[2]]; net_send(cmd+" "+creds[0]+" "+creds[1],function(ok,err){ if(err){ addRoomEntry(roomid,"error",[now(),"Unable to "+cmd+": "+err]); } else if(cmd=="register"){ addRoomEntry(roomid,"notice",[now(),"Successfully registered user '"+creds[0]+"'"]); } else { username=creds[0]; addRoomEntry(roomid,"notice",[now(),"Logged in as user '"+creds[0]+"'"]); updateStatus(); fetchRoomList(); } }); break; case "createroom": if(spl.word.length!=1){ showUsage(roomid,cmd); break; } if(!username){ addRoomEntry(roomid,"error",[now(),"createroom: not logged in"]); break; } net_send("create_room",function(name,err){ if(err){ addRoomEntry(roomid,"error",[now(),"Unable to create a room: "+err]); } else { addRoomEntry(roomid,"notice",[now(),"Successfully created room '"+name+"'"]); fetchRoomList(); } }); break; case "invite": if(spl.word.length<2){ showUsage(roomid,cmd); break; } if(!username){ addRoomEntry(roomid,"error",[now(),"invite: not logged in"]); break; } if(!currentroom||currentroom[0]==":"){ addRoomEntry(roomid,"error",[now(),"invite: cannot invite into room '"+currentroom+"'"]); break; } othername=spl.rest[1]; net_send("invite "+currentroom+" "+othername,function(ok,err){ if(err){ addRoomEntry(roomid,"error",[now(),"Unable to invite '"+othername+"': "+err]); } else { addRoomEntry(roomid,"notice",[now(),"Successfully invited '"+othername+"'"]); } }); break; case "leave": if(!username){ addRoomEntry(roomid,"error",[now(),"invite: not logged in"]); break; } var roomtoleave=currentroom; if(!roomtoleave||roomtoleave[0]==":"){ addRoomEntry(roomid,"error",[now(),"invite: cannot leave room '"+roomtoleave+"'"]); break; } net_send("leave_room "+roomtoleave,function(ok,err){ if(err){ addRoomEntry(roomid,"error",[now(),"Unable to leave this room: "+err]); } else { uiLeaveRoom(roomtoleave); } }); break; case "shrug": sendMessage(roomid,"¯\\_(ツ)_/¯"); break; case "help": for(var i=0;i<commandlist.length;i++){ addRoomEntry(roomid,"notice",[now(),commandlist[i].usage]); } break; default: addRoomEntry(roomid,"error",[now(),"Unknown command '"+cmd+"'"]); break; } } function sendMessage(roomid,text){ if(roomid[0]==":"){ addRoomEntry(roomid,"error",[now(),"Cannot send a message here"]); return; } var sentAs=username,msg=text.replace(/\n/g,""); var replyid=currentreply?currentreply[0]:"-1"; cancelReply(); net_send("send "+roomid+" "+replyid+" "+msg,function(msgid,err){ if(msgid!=null){ msgid=msgid.toString(); addRoomEntry(roomid,"message",[sentAs,new Date().getTime(),msgid,replyid,msg]); return; } addRoomEntry(roomid,"error",[now(),"Unable to send message: "+err]); }); } function doSend(){ var inputelem=document.getElementById("roominput"); var text=inputelem.value; inputelem.value=""; if(text.length==0)return; if(text.length>=2&&text[0]=="/"&&text[1]=="/"){ sendMessage(currentroom,text.slice(1)); } else if(text[0]=="/"){ executeCommand(currentroom,text.slice(1)); } else { sendMessage(currentroom,text); } } function doKeypress(ev){ if(ev.keyCode==10||ev.keyCode==13)doSend(); } window.addEventListener("load",function(){ reconnect(); drawRoom(currentroom); document.getElementById("roominput").focus(); }); </script> <style> html, body{ margin:0; width:100%; height:100%; } body{ background-color:#04040c; color:#eee; font-family:mononoki,meslo,monaco,monospace; font-size:11pt; } table{ border-collapse:collapse; } #window{ width:100%; height:100%; } .invisible{ display:none; } /* SIDEBAR */ #sidebar{ width:150px; border-right:1px #668 solid; vertical-align:top; } #branding{ background-color:#223; padding:4px; height:22px; text-align:center; font-size:13pt; } #status{ padding:2px; margin:8px 0px 10px 0px; } .roomlistitem{ margin:2px 0px; padding:3px 2px; background-color:#223; cursor:pointer; } .roomlistitem:hover{ background-color:#282833; } .roomlistitem.selected{ background-color:#335; font-weight:bold; } .roomlistitem:hover{ background-color:#445; } /* ROOM VIEW */ #room{ width:100%; height:100%; } #room_td{ padding:0; } #roomtitle{ height:22px; padding:4px; background-color:#335; font-weight:bold; } #roomlog_td{ vertical-align:top; } #roomlog_table{ width:100%; } #roomlog{ /* The scrolling-table hack */ display:block; overflow-y:auto; height:calc(100vh - 63px); /* TODO: figure out whether 63 is ALWAYS correct */ } #roomlog > tr{ vertical-align:top; } #roomlog > tr > td:first-child{ width:80px; color:#666; } #roomlog > tr > td:nth-child(2){ width:120px; padding-right:20px; text-align:right; } #roomlog > tr > td:last-child{ white-space:pre-wrap; } #roomlog > tr.message{ color:inherit; cursor:pointer; } #roomlog > tr.message:hover{ background-color:#202030; } #roomlog > tr.replying{ background-color:#090 !important; } #roomlog > tr.error{ color:#f44; } #roomlog > tr.notice{ color:#a8f; } #roomlog > tr > td > span.reply{ color:#8f8; font-style:italic; } #roomlog > tr > td > span.reply.pending:after{ content:"(...)"; } #roomlog > tr > td > span.reply.failed{ color:#f22; font-style:normal; } #roombar_tr{ height:30px; border-top:1px #668 solid; } #roominput_td{ position:relative; vertical-align:top; padding:0px 1px 0px 10px; } #roominput_td.replying{ background-color:#090; } #roominput{ position:absolute; background-color:rgba(0,0,0,0); padding:0; border-width:0; width:calc(100% - 11px); height:100%; color:white; font-family:inherit; font-size:inherit; } #roomsend_td{ position:relative; width:40px; vertical-align:top; padding:0; } #roomsend{ position:absolute; width:100%; height:100%; background-color:#335; border-width:0px; color:#ccc; font-family:inherit; font-size:inherit; cursor:pointer; } #roomsend:hover{ background-color:#445; } </style> </head> <body> <table id="window"><tbody><tr> <td id="sidebar"> <div id="branding">TOMSG</div> <div id="status"> </div> <div id="roomlist"></div> </td> <td id="room_td"> <table id="room"><tbody> <tr id="roomtitle_tr"><td id="roomtitle" colspan="2"></td></tr> <tr id="roomlog_tr"><td id="roomlog_td" colspan="2"> <table id="roomlog_table"><tbody id="roomlog"></tbody></table> </td></tr> <tr id="roombar_tr"> <td id="roominput_td"><input type="text" id="roominput" onkeypress="doKeypress(event)"></td> <td id="roomsend_td"> <button id="roomsend" onclick="doSend()"> <svg width="27px" height="21px" version="1.1" viewBox="10 15 100 85" style="margin-top: 6px; margin-bottom: -2px"> <g fill="#fff"> <path transform="translate(-.61382 -.30051)" d="m14.614 20.301 76 30-72-5z"/> <path d="m14 80 76-30-72 5z"/> </g> </svg> </button> </td> </tr> </tbody></table> </td> </tr></tbody></table> </body> </html>