<!doctype html> <html> <head> <meta charset="utf-8"> <title>tomsg webclient</title> <script> var sock=null,username=null; var roomlist=[":console"]; var currentroom=":console"; var roomlogs=new Map([[":console",[]]]); 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(err); } else { list.push(item); if(list.length==count){ cb(list); } else { net_callbacks[id]=net_historyCollectionCallback.bind(this,id,list,count,cb); } } } function reconnect(){ if(sock)sock.close(); net_callbacks={}; 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); addRoomEntry(r,"message",[u,t,spl.rest[5]]); } else if(type=="invite"){ var r=spl.word[2]; roomlist.push(r); roomlogs.set(r,[]); 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=="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=="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.rest[6]]); else alert("Unknown server response message type '"+type+"'!"); } else { alert("No callback for server message id '"+id+"'!"); } }); sock.addEventListener("open",function(){ 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 updateStatus(){ var str=null; if(!sock||sock.readyState>=2){ str="not connected"; } else if(sock.readyState==0){ str="connecting..."; } 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 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": tr.classList.add("message"); node0.nodeValue=formatTime(new Date(args[1])); node1.nodeValue="<"+args[0]+">"; node2.nodeValue=args[2]; break; case "error": tr.classList.add("error"); node0.nodeValue=formatTime(new Date(args[0])); node1.nodeValue="--"; node2.nodeValue=args[1]; break; case "notice": 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 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){ addRoomEntry(roomid,"error",[now(),"Usage: /"+cmd+" <username> <password>"]); 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){ addRoomEntry(roomid,"error",[now(),"Usage: /createroom"]); 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){ addRoomEntry(roomid,"error",[now(),"Usage: /invite <username> (adds to current room)"]); 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 "shrug": sendMessage(roomid,"¯\\_(ツ)_/¯"); 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,""); net_send("send "+roomid+" "+msg,function(ok,err){ if(ok){ addRoomEntry(roomid,"message",[sentAs,new Date().getTime(),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%; } /* 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; } #roomlog > tr.error{ color:#f44; } #roomlog > tr.notice{ color:#a8f; } #roombar_tr{ height:30px; border-top:1px #668 solid; } #roominput_td{ position:relative; vertical-align:top; padding:0px 1px 0px 10px; } #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> <script src="https://use.fontawesome.com/ccf28a5626.js"></script> </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()"><i class="fa fa-send"></i></button> </td> </tr> </tbody></table> </td> </tr></tbody></table> </body> </html>