message this user

Name

chat

Readme

Chat

This is a chat based on webrtc.
To run it add chat to your build and add startChat() to your init.go to
This is also the default configured application for new users.
Because of this it is actualy configured to run wchat which may be a better chat implementation.
Running /app/chat/room will actually launch wchat.

Global

#import getscript
#import parallel
#import peer
#import jquery

function isconnchatserver(conn,csid,peer,cb) {
 console.log('isconnchatserver',conn);
 var cbd = false;
 var timeout;
 conn.send(JSON.stringify({cmd:'register',user:user}));
 conn.on('data',data=>{
  data = JSON.parse(data);
  if(!cbd && data.cmd=='registered') {
   console.log('Registered successfully');
   cbd = true;
   clearTimeout(timeout);
   cb(data.registered);
  }
 });
 timeout=setTimeout(()=>{
  cbd = true;
  cb(false);
 },1000); //We did not get a useful response for too long.
}


function connecttocsidasserver(csid,peer,cb) {
 console.log('connecttocsidasserver',csid,peer);
 //returns (err,connection)
 var conn = peer.connect(csid);
 conn.on('open',()=>{
  isconnchatserver(conn,csid,peer,yes=>{
   if(!yes) { return cb('Is not a chat server'); }
   cb(null,conn);
  });
 });
 peer.on('error',error=>{
  if(error.message === 'Could not connect to peer '+csid) {
   cb(error); 
  }
 });
}

function connecttopeerasserver(peername,peer,cb) {
 console.log('connecttopeerasserver',peername,peer);
 $.post('/peer/getid/'+peername,csid=>{
  if(!csid) {
   return cb('Peer does not have id');
  }
  connecttocsidasserver(csid,peer,cb);
 });
}

function runityourself(servername,peer,cb) {
 console.log('runityourself',peer);
 $.post('/announce/chat-server/'+servername);
 var iframe = $('<iframe>').attr('src','/app/chat-server');
 $('body').append(iframe);
 setTimeout(()=>{
  connecttopeerasserver(window.user,peer,(error,conn)=>{
   if(error) { return console.log('iframe error',error); }
   cb(conn);
  })
 },5000);
}

function tryconnectingtopeers(servername,peer,cb) {
 console.log('tryconectingtopeers',servername,peer);
 $.post('/findpeers/chat-server/'+servername,peerlist=>{
  peerlist = peerlist.reverse();
  console.log('peerlist',peerlist);
  mapserialuntilsuccess(peerlist,(trypeer,cb)=>{
   if(trypeer == window.user) { return cb('Can\'t connect to self at this stage.'); }
   console.log('trypeer',trypeer);
   connecttopeerasserver(trypeer,peer,cb);
  },cb);
 });
}


function get_chat_server_conn(servername,cb,first) {
 var registernewpeercount=0;
 registernewpeer(peer=>{
  console.log('registernewpeercount',++registernewpeercount);
  window.peer = peer;
  peer.on('error',error=>{
   console.log('peer error:',error);
   window.args = error;
  });
  peer.on('connection',conn=>{
   conn.send(JSON.stringify({cmd:'registered',registered:'false'})); //Reject peers right away so they can find good peers quicker;
  });
  var csidcount=0;
  connecttopeerasserver(servername,peer,(error,conn)=>{
   if(error) {
    return tryconnectingtopeers(servername,peer,(error,conn)=>{
     console.log('returned from tryconnectedtopeers');
     if(error) {
      console.log('error causes runityourself',error);
      return runityourself(servername,peer,cb); 
     }
     cb(conn);
    });
   }
   cb(conn);
  });
 });
}

function startChat(servername) {
 servername = location.pathname.replace(/\/run/,'/').replace(/\/app\/\w+\/?/,'/').split('/').slice('-1')[0] || servername || 'chat-server';
 console.log('servername',servername);
 init_chat_ui();
 getjquery(()=>{
  
 });
 get_chat_server_conn(servername,conn=>{
  console.log('conn',conn);
  $('h3').text('Chatting');
  add_user_to_ui(user);
  function sendclick (e) {
   var msg=$('#compose').val().trim();
   if(msg.length===0) {
    return;
   }
   conn.send(JSON.stringify({cmd:'post',user:user,msg:msg}));
   add_msg_to_ui(user,msg);
   $('#compose').val('');
  }
  $('#send').click(sendclick);
  $('#compose').on('keydown',e=>{
   if(e.keyCode==13) {
    e.preventDefault();
    sendclick(e);
   }
  });
  conn.on('data',handle_server_msg);
 });
}

function getchathtml () {
 return `
  <div class="dialogform" title="Embed">
   <input class="image hide" id="imagetyperadio" type="radio" name="type" value="jpg"/>
   <label for="imagetyperadio" class="hide">Image</label>
   <input class="video" id="videotyperadio" type="radio" name="type" value="mp4"/>
   <label for="videotyperadio">Video</label>
   <input class="poll" id="polltyperadio" type="radio" name="type" value="poll"/>
   <lable for="polltyperadio">Poll</lable><br/>
   <input class="title" placeholder="Title (optional)"/><br class="onvideo onimage"/>
   <input class="onimage onvideo url" placeholder="Url"/><br class="onvideo"/>
   <input class="onvideo" id="syncbox" type="checkbox"/>
   <label class="onvideo" for="syncbox">Synchonized</label><br class="onsync"/>
   <input class="onsync" id="loopbox" type="checkbox"/>
   <label class="onsync" for="loopbox">Loop</label><br class="onsync"/>
   <input class="onsync delay" placeholder="Delay, use h,m,s suffixes"/><br class="onpoll"/>
   <input class="onpoll polloption" placeholder="Option"/><br/>
  <button>Post</button>
  </div>
  <div class="flow flowv container">
   <div class="flow flowh chat">
    <table class="content flow flowv">
     <tbody>
     </tbody>
    </table>
    <div class="flow flowv users" id="joined">
     <h3>Connecting</h3>
     <ul>
     </ul>
    </div>
   </div>
   <div class="flow flowh compose">
    <span class="self" title="Embed"></span>
    <textarea id='compose' placeholder='Send message...'></textarea>
    <button id='send' class='hide'>Send</button>
   </div>
  </div>
  <div class="side flow flowv">
  </div>`
}


function media_filters () {
 return [video_msg_filter,poll_msg_filter];
}

function media_message_to_td(msg,user) {
 console.log('msg',msg);
 var media_attributes=JSON.parse(msg.replace('::{"','{"'));
 var msgtd;
 media_filters().some(mf=>{
  var [handled,tmpmsgtd]=mf(media_attributes,user);
  if(handled) {
   msgtd=tmpmsgtd;
   return true;
  }
  return false;
 });
 return msgtd;
}

function video_msg_filter(attr,user) {
 if(attr.type==='mp4') {
  var td = $('<td>').html((attr.title||attr.url)+'<button class="embedbutton">embed'+(attr.sync?' sync':'')+(attr.loop?' loop':'')+'</button>');
  td.css('color','blue').css('text-decoration','underline').css('cursor','pointer');
  td.click(()=>{
   display_mp4_in_chat(attr.url,attr.sync?attr.start:null,attr.loop);
  });
  return [true,td];
 }
 return [false,null];
}

function poll_msg_filter(attr,user) {
 if(attr.type=='poll') {
  var td = $('<td>').attr('id','poll'+attr.id);
  td[0].votes=[];
  var title = $('<h3>').text('Poll: '+attr.title)
     .css('font-size','24px')
     .css('font-family','Helvetica Neue",Helvetica,Arial,sans-serif')
     .css('font-weight','600')
     .css('color','#068f06')
     .css('margin','16px 0 8px 0');
  td.append(title);
  var table = $('<table>');
  var tbody = $('<tbody>');
  table.append(tbody);
  attr.options.forEach((o,i)=>{
   var clicked = false;
   var tr=$('<tr>').css('cursor','pointer');
   var count=$('<td>').text(0);
   var option = $('<td>').text(o);
   td[0].votes[i]={};
   tbody.append(tr.append(count,option));
   tr.click(()=>{
    var retr={};
    retr.type='pollvote'
    retr.forid=attr.id;
    retr.for=i;
    if(clicked) {
     retr.nullify=true;
    }
    clicked=!clicked;  
    $('#compose').val('::'+JSON.stringify(retr));
    $('#send').click();
   });
  });
  td.append(table);
  return [true,td];
 }
 else if(attr.type=='pollvote') {
  var polltd = $('#poll'+attr.forid)
  var votes = polltd[0].votes[attr.for];
  if(attr.nullify) {
   delete votes[user];
  }
  else {
   votes[user]=true;
  }
  polltd.find(`tr:nth-child(${attr.for+1}) td`).first().text(Object.keys(votes).length);
  return [true,null];
 }
 else {
  return [false,null];
 }
}

function add_msg_to_ui(user,msg) {
 var msgtd;
 if(msg.startsWith('::{"')) {
  msgtd = media_message_to_td(msg,user);
 }
 else {
  msgtd = $('<td>').html(msg).addClass('msg');
 }
 if(!msgtd) {
  return;
 }
 var table = $('.chat > table.content');
 var tr = $('<tr>').attr('data-user',user);
 var usertext;
 if(table.find('tr').last().attr('data-user')==user) {
  usertext="";
 }
 else {
  usertext=user;
 }
 var usertd = $('<td>').append($('<a>').attr('href','/u/'+user).attr('target','_blank').html(usertext)).addClass('user');
 
 var t1 = table[0];
 var isbottom = t1.scrollTop === t1.scrollTopMax;
 table.find('>tbody').append(tr.append(usertd).append(msgtd));
 if(isbottom) {
  t1.scrollTop = t1.scrollTopMax; 
 }
}

function add_user_to_ui(user) {
 if($('#user_'+user).length===0) {
  $('#joined ul').append($('<li>').attr('id','user_'+user).append($('<a>').attr('href','/u/'+user).attr('target','_blank').html(user+(user==window.user?'(you)':''))));
  if(user==window.user) {
   $('.compose .self').append($('<span>').html(user));
  } 
 }
}

function remove_user_from_ui(user) {
 $('#joined #user_'+user).remove();
}



function handle_server_msg(data) {
 data=JSON.parse(data);
 console.log('revieved',data);
 if(data.cmd=='post'&&data.msg&&data.user) {
  add_msg_to_ui(data.user,data.msg);
 }
 else if(data.cmd=='left'&&data.user) {
  remove_user_from_ui(data.user);
 }
 else if(data.cmd=='joined'&&data.user) {
  add_user_to_ui(data.user);
 }
 else if(data.cmd=='posts'&&data.msg) {
  data.msg.forEach(msg=>{
   add_msg_to_ui(msg.user,msg.msg);
  });
 }
 else if(data.cmd=='peerlist'&&data.msg) {
  data.msg.forEach(add_user_to_ui);
 }
}

function getchatcss () {
  return `
/*@import https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css;
@import https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.structure.min.css;
@import https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.theme.min.css;*/
html,body {
  width:100%;
  height:100%;
  margin:0;
  padding:0;
  display:flex;
  font-family:sans;
}
ul {
  list-style-type: none;
  padding:0;
  margin:0;
  height:auto;
}
.flow {
  display:flex;
}
.flowv {
  flex-flow:column;
}
.flowh {
  flex-flow:row;
}
.users {
}
.chat {
  flex: 1 1 auto
}
.content {
  flex:1 1 auto;
  overflow-y:scroll;
}

.compose {
  background:#5668b5;
  height:2em;
  border:8px solid #f6f6f6;
}
.compose .self {
 background: #f0f0f0;
 padding: 4px 16px;
 margin: 2px;
 align-content: center;
 cursor:pointer
}
.compose textarea {
 flex: 1 1 auto;
 resize:none;
 padding-top:4px;
 padding-left:4px;
}
.indent {
  padding-left:74px;
}
td {
 vertical-align:middle;
 padding-left:8px;
}
.content .user {
 width:75px;
 overflow-y:wrap;
 text-align:right;
 color:green;
}
.container {
  width:100%;
}
body.openside .container {
  max-width:50%;
}
body.openside .chat {
  flex-direction:row-reverse;
}
#joined {
  width:200px;
  background-color:#f6f6f6;
}
#joined a {
 color:black;
 text-decoration:none;
}
#joined a:hover {
  text-decoration:underline;
}
/*.self .more {
 display:none;
 position:absolute;
 margin-top:0;
 transition:margin-top 0.5s;
 height:100px;
 width:100px;
}
.more.show {
 display:block;
 margin-top:-37px;
}
.more li:hover {
 background:lightblue;
}*/
.dialogform {
 display:none;
}
video {
  max-width:100%;
  max-height:100%;
  flex-shrink:1;
}
.side {
  font-size:1.8em;
}
.side i {
  background:#ffffff4d;
}
.side i:hover {
  font-size:1.4em;
}
.side .control {
 transition:opacity 0.4s;
}
.side.hasinteracted .control {
  opacity:0;
}
.side .control:hover {
 font-size:0.8823529411764706em;
 opacity:1;
}
.hide {
  display:none;
}
.container > .chat {
 overflow-y:scroll;
}
.dialogform .onimage, .dialogform .onvideo, .dialogform .onpoll, .dialogform .onsync {
  display:none;
}
.dialogform.onimage .onimage, .dialogform.onvideo .onvideo, .dialogform.onpoll .onpoll, .dialogform.onvideo.onsync .onsync {
  display:inline;
}
td.user {
 max-width:128px;
 overflow:hidden;
}
td.msg {
 min-width:256px;
}
body.openside #joined {
  overflow:hidden;
}
iframe {
 display:none;
}
`
}

function register_embed_form_listeners () {
 $('#imagetyperadio').click(()=>{
  $('.dialogform').addClass('onimage')
                  .removeClass('onvideo')
                  .removeClass('onpoll');
 });
 $('#videotyperadio').click(()=>{
  $('.dialogform').addClass('onvideo')
                  .removeClass('onimage')
                  .removeClass('onpoll');
 });
 $('#polltyperadio').click(()=>{
  $('.dialogform').addClass('onpoll')
                  .removeClass('onimage')
                  .removeClass('onvideo');
 });
 $('#syncbox').click(()=>{
  if($('#syncbox').prop('checked')) {
   $('.dialogform').addClass('onsync');
  }
  else {
   $('#syncbox').removeClass('onsync');
  }
 });
 $('.dialogform .polloption')[0].addEventListener('keydown',bottomoptionkeydown);
 $('.dialogform button').click(()=>{
  console.log('clicked');
  var embedobject = makeembedobject();
  if(!embedobject) {
   return;
  }
  $('#compose').val('::'+JSON.stringify(embedobject));
  //sendclick();
  $('#send').click();
  $('.ui-dialog-titlebar-close').click();
 });
}

function bottomoptionkeydown () {
 var newoption = $('<input>').addClass('polloption')
                             .addClass('onpoll')
                             .attr('placeholder','Option');
 var lastoption = $(this);
 var br = $('<br>').addClass('onpoll');
 lastoption[0].removeEventListener('keydown',bottomoptionkeydown);
 newoption[0].addEventListener('keydown',bottomoptionkeydown);
 lastoption.change(()=>{
  if(lastoption.val().length===0) {
   lastoption.remove();
   br.remove();
  }
 });
 lastoption.after(newoption);
 lastoption.after(br);
}


function makeembedobject () {
 var retr = {};
 var form = $('.dialogform');
 var thetype=form.find("input[name='type']:checked").val();
 if(!thetype) {
  alert('Please select a type');
  return null;
 }
 retr['type']=thetype;
 var title=form.find('.title').val();
 if(title) {
  retr.title = title;
 }
 if(thetype=='jpg'||thetype=='mp4') {
  var url=form.find('.url').val();
  if(!url) {
   alert('Missing URL');
   return null;
  }
  retr.url = url;
 }
 if(thetype=='mp4') {
  if($('#syncbox').prop('checked')) {
   retr.sync=true;
   if($('#loopbox').prop('checked')) {
    retr.loop=true;
   }
   var delay=form.find('.delay').val();
   var seconds=0;
   delay.split(' ').forEach(i=>{
    var value = parseFloat(i);
    var mult = 1;
    if(isNaN(value)) {
     return;
    }
    switch(i.slice(-1)) {
     case 'w':
      mult*=7;
     case 'd':
      mult*=24;
     case 'h':
      mult*=60;
     case 'm':
      mult*=60
    }
    seconds+=value*mult;
   });
   retr.start = Date.now()+seconds*1000;
  }
 }
 if(thetype=='poll') {
  retr.id = Math.floor(Math.random()*10000);
  retr.options = form.find('.polloption').toArray().map(e=>{
   return e.value
  }).filter(e=>{
   return e.length;
  }); 
 }
 form.find('input').empty();
 ['image,video,poll,sync'].forEach(kind=>{
  form.removeClass('on'+kind);
 });
 retr.target='side';
 return retr;
}

function init_chat_ui () {
    //We want a user list on the right
    //We want a text bar on the bottom
    //We want a content on the top
 add_style(getchatcss());
 setTimeout(()=>{
  document.body.innerHTML=getchathtml();
  getcss('https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css');
  getcss('https://use.fontawesome.com/releases/v5.6.3/css/all.css');
  getscripts(['https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js','https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'],()=>{
   register_embed_form_listeners();
   $('.compose .self').click(()=>{
     $('.dialogform').dialog();
   });
  });
  //add_ui_listeners();
 },50);
}

function start (servername) {
 startChat(servername);
}

function display_mp4_in_chat(mp4url,start,loop) {
 var video = $('<video>').attr('src',mp4url).attr('controls',true).attr('autoplay',true).attr('loop',loop);
 $('body').addClass('flex').addClass('flowh').addClass('openside');
 var controls = $('<div>').addClass('control')
       .attr('position','absolute')
       .css('width','100%')
       .css('height','0')
       .css('text-align','right')
       .css('z-index',1);
 var close = $('<i>').addClass('far').addClass('fa-window-close');
 var grow = $('<i>').addClass('far').addClass('fa-plus-square');
 var shrink = $('<i>').addClass('far').addClass('fa-minus-square');
 $('body > .side').prepend(video);
 $('body > .side').prepend(controls.append(shrink).append(grow).append(close));
 close.click(()=>{
  controls.remove();
  video.remove();
  if($('body > .side').children().length===0) {
   $('body').removeClass('openside');
   $('body .container').css('max-width','');
   $('body > .side').removeClass('hasinteracted');
  }
 });
 grow.click(()=>{
  $('.container').css('max-width',parseInt($('.container').css('max-width'))-10+'%');
 });
 shrink.click(()=>{
  $('.container').css('max-width',parseInt($('.container').css('max-width'))+10+'%');
 });
 controls.find('i').mouseover(()=>{
  $('.side').addClass('hasinteracted');
 });
 var initialnow = Date.now();
 function resync () {
  if(start>Date.now()) {
   video[0].pause();
   video[0].controls=false;
   setTimeout(resync,start-Date.now());
   setTimeout(()=>{
    video[0].play();
    video[0].controls=true;
    initialnow = start;
   },start-Date.now());
   return;
  }
  var currentTime = ((Date.now()-start)%(loop?video[0].duration*1000:Infinity))/1000;
  video[0].currentTime = currentTime;
  console.log('offset set',currentTime,start);
  setTimeout(resync,(Date.now()-initialnow)*3);
 }
 if(start) {
  video[0].addEventListener('loadedmetadata',resync,false);
 }
}

Init

startChat('');

Build

wchat