message this user

Name

wchat

Global

#import getscript
#import parallel
#import jquery
#import lodash
#import ytplayer
#import marked
#import subchannel

function testbottom() {
 var t1 = $('table.content')[0];
 return t1.scrollTopMax && t1.scrollTop === t1.scrollTopMax || t1.scrollHeight-t1.scrollTop-t1.offsetHeight<=5;
}
function restorebottom(bottom) {
 if(bottom) {
  var t1 = $('table.content')[0]
  t1.scrollTop = t1.scrollTopMax || t1.scrollHeight;
  var interval = setInterval(()=>t1.scrollTop = t1.scrollTopMax || t1.scrollHeight,10);
  setTimeout(()=>{
   clearInterval(interval);
  },500);
 }
}

function focuschanged(state) {
 console.log('focus changed',state);
 if(state=='focus') {
  window.hasfocus=true;
  if(window.myonfoci) {
   while(window.myonfoci.length) {
    window.myonfoci.shift()();
   }
  }
 }
 else {
  window.hasfocus=false;
 }
 //$('#compose').val($('#compose').val()+' '+state);
}

function announcetoserver(roomname) {
 getjquery(()=>{
  $.post('/announce/wchat/'+roomname);
 });
}

function announcetopeerlist(peers,notrim) {
 var room=window.roomname;
 peers = peers.filter(peer=>{
  return peer!=window.user;
 });
 if(!notrim) {
  peers = peers.filter(peer=>{
   return !window.knownpeers[peer];
  });
 }
 window.ws.send(JSON.stringify({to:peers,msg:{cmd:'announce',room:room,nonce:window.sessionid}}));
 window.failedtosendlock = true;
 //Find a random peer that responded to ask if they know any more after two seconds
 //Load lodash in the meantime.
 parallel([cb=>{
  setTimeout(cb,2000);
 },getlodash],cb=>{
  peers = _.shuffle(peers);
  for(var i=0;i<peers.length;++i) {
   //Find a peer that announced back in within 2 seconds and ask for more peers;
   if(window.knownpeers[peers[i]]) {
    //They announced back;
    console.log('Requesting posts and peers from',peers[i]);
    window.ws.send(JSON.stringify({to:peers[i],msg:{cmd:'requestpeers',room:room}}));
    return;
   }
  }
  //If no one new responded that's it.  We have our peers.
  window.allpostsgathered=true;
 });
}

function announcetopeers(roomname) {
 window.knownpeers = window.knownpeers || {};
 getjquery(()=>{
  $.post('/findpeers/wchat/'+roomname,peerlist=>{
   announcetopeerlist(peerlist);
   peerlist.forEach(peer=>{
    if(peer!=window.user) {
     getpostsfrompkey(roomname,null,peer);
    }
   });
  });
 });
}


function wsonmessage(message) {
 console.log('message',message);
 message=JSON.parse(message.data);
 if(message.failedtosend) {
  window.failedtosend = message.failedtosend;
  window.failedtosendlock = false;
  return;
 }
 var data=message.msg;
 console.log('revieved',data);
 if(data.room!=window.roomname) {
  console.log('Ignored, wrong room',data.room,window.roomname,data);
  return;
 }
 if(data.cmd=='announce' && data.room==window.roomname) {
  if(window.knownpeers[message.from]!=data.nonce) {
   window.knownpeers[message.from]=data.nonce;
   add_user_to_ui(message.from);
   announcetopeerlist([message.from],true);
  }
  //We don't have to do anything because we are treating every valid message as an announce.
  //It's enought to note this as valid with an if statement.
 }
 else if(data.cmd=='requestpeers'&&data.room===window.roomname) {
  window.posts = window.posts || {};
  window.ws.send(JSON.stringify({to:message.from,msg:{cmd:'peerlist',room:window.roomname,msg:Object.keys(window.knownpeers)}}));
  var sendlist=Object.keys(window.posts).sort((a,b)=>a-b).map(i=>window.posts[i]);
  window.ws.send(JSON.stringify({to:message.from,msg:{cmd:'posts',room:window.roomname,msg:sendlist}}));
 }
 else if(data.cmd=='post'&&data.msg&&message.from) {
  add_msg_to_ui(message.from,data.msg,data.nonce);
 }
 else if(data.cmd=='left'&&message.from) {
  delete window.knownpeers[message.from];
  remove_user_from_ui(message.from);
 }
 else if(data.cmd=='posts'&&data.msg&&message.from) {
  data.msg.forEach(msg=>{
   add_msg_to_ui(msg.user,msg.msg,msg.nonce);
  });
 }
 else if(data.cmd=='peerlist'&&data.msg) {
  announcetopeerlist(data.msg)
 }
 else {
  //Must have been invalid.
  return;
 }
 if(!window.knownpeers[message.from]) {
  //We got a message from someone we don't know.
  //That could be bad.
  //Let's add them.
  window.knownpeers[message.from]=true;
  add_user_to_ui(message.from);
  announcetopeerlist([message.from]);
 }
}

function sendmessagetopeers(msg,nonce) {
 nonce=nonce||Date.now();
 window.ws.send(JSON.stringify({to:Object.keys(window.knownpeers),msg:{cmd:'post',room:window.roomname,user:window.user,msg,nonce}}));
}

function sendclick (e) {
 var msg=$('#compose').val().trim();
 $('#compose').val('');
 if(msg.length===0) {
  return;
 }
 var nonce=Date.now();
 sendmessagetopeers(msg,nonce);
 add_msg_to_ui(user,msg,nonce);
}

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');
  getjquery(()=>{
   register_embed_form_listeners();
   $('#send').click(sendclick);
   $('#compose').on('keydown',e=>{
    if(e.keyCode==13 && !e.shiftKey) {
     e.preventDefault();
     sendclick(e);
    }
   });
   getjqueryui(()=>{
    $('.compose .self').click(()=>{
      $('.dialogform').dialog();
    });
   });
  });
  //add_ui_listeners();
 },50);
}

function openwebsocket() {
 window.ws = new WebSocket(location.protocol.replace('http','ws')+'//'+location.host+'/peerws');
 window.ws.onmessage = wsonmessage;
 window.ws.onclose = ()=>{
  if(window.hasfocus) {
   openwebsocket();
  }
  else {
   window.myonfoci = window.myonfoci || [];
   window.myonfoci.push(openwebsocket);
  }
 }
 window.ws.onopen = ()=>{
  getjquery(()=>{
   $('#status').text('Chatting');
  });
  announcetoserver(window.roomname);
  announcetopeers(window.roomname);
  //getpostsfrompkey(window.roomname);
  add_user_to_ui(user);
 };
}

function startChat(servername) {
 //var roomname = location.pathname.replace(/\/run/,'/').replace(/\/app\/\w+\/?/,'/').split('/').slice('-1')[0] || servername || 'wchat';
 var roomname = appsubchannel() || servername || 'wchat';
 if(roomname.startsWith('/')) {
  roomname=roomname.replace('/','');
 }
 window.sessionid=Math.round(Math.random()*100000);
 window.roomname = roomname;
 console.log('roomname',roomname);
 getmarked(()=>{});
 init_chat_ui();
 getpostsfrompkey(roomname);
 openwebsocket();
 window.hasfocus=false;
 window.addEventListener('blur',()=>{
  focuschanged('blur');
 });
 window.addEventListener('focus',()=>{
  focuschanged('focus');
 });
}

function getchatcss () {
  return `
html,body {
  width:100%;
  height:100%;
  margin:0;
  padding:0;
  display:flex;
  font-family:'Nunito', Arial, Helvetica, Sans-Serif;
}
body.openside table.content > tbody > tr {
 display:flex;
 flex-flow:column;
}
body.openside td.user {
 text-align:left;
}
ul {
  list-style-type: none;
  padding:0;
  margin:0;
  height:auto;
  font-size:14px;
}
.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;
  font-size:14px;
}

.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:top;
 padding-left:8px;
}
.content .user {
 overflow-y:wrap;
 text-align:right;
 color:green;
}
.container {
  width:100%;
}
body.openside .container {
  max-width:50%;
}
body.openside .chat {
  flex-direction:row-reverse;
}
#sidebar {
  width:200px;
  background-color:#f6f6f6;
}
#sidebar a {
 color:black;
 text-decoration:none;
}
#sidebar 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;
  width:100%;
  display:none;
}
.side i {
  background:#ffffff4d;
}
.side i:hover {
  font-size:1.4em;
}
.side .mirror {
 transition:opacity 0.4s;
}
.side.hasinteracted .mirror {
  opacity:0;
}
.side .mirror:hover {
 font-size:0.8823529411764706em;
 opacity:1;
}
.hide {
  display:none;
}
.container > .chat {
 overflow-y:scroll;
}
.dialogform .onimage, .dialogform .onvideo, .dialogform .onpoll, .dialogform .onsync, .dialogform .onavatar, .dialogform .onupload {
  display:none;
}
.dialogform.onimage .onimage, .dialogform.onvideo .onvideo, .dialogform.onpoll .onpoll, .dialogform.onavatar .onavatar, .dialogform.onupload .onupload, .dialogform.onvideo.onsync .onsync {
  display:inline;
}
td.user {
 max-width:128px;
 overflow:hidden;
 text-overflow: ellipsis;
}
td.msg {
 min-width:256px;
}
body.openside #sidebar {
  overflow:hidden;
  text-overflow: ellipsis;
}
body.openside .side {
  display:flex;
}
iframe {
 display:none;
}
p {
 margin:0;
}
#mirrorheader {
 display:none;
}
#controllers > li {
 cursor:pointer;
}
#broadcast {
 display:none;
 cursor:pointer;
 font-size:0.7em;
}
.avatar {
 width: 32px;
 height: 32px;
 border-radius: 2000px;
 visibility:hidden;
 margin-right:3px;
}
#joined a {
 display:flex;
 align-items:center;
}
`
}

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"/>
   <label for="polltyperadio">Poll</label>
   <input class="mirror" id="mirrortyperadio" type="radio" name="type" value="mirror"/>
   <label for="mirrortyperadio">Mirror</label>
   <br />
   <input id="avatartyperadio" type="radio" name="type" value="avatar"/>
   <label for="avatartyperadio">Avatar</label>
   <input id="uploadtyperadio" type="radio" name="type" value="upload"/>
   <label for="uploadtyperadio">Upload</label>
   <br />
   <input class="title onvideo onpoll" placeholder="Title (optional)"/><br class="onvideo onimage"/>
   <input class="url onimage onvideo onavatar" 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"/>
   <a class="onupload" href="/app/upload" style="color:blue" target="_blank"><br>Head to the upload app<br></a><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="sidebar">
     <h3 id="status">Connecting</h3>
     <ul id="joined"></ul>
     <h3 id="mirrorheader">Mirror</h3>
     <ul id="controllers"></ul>
     <h4 id="broadcast" title="Click to toggle">Broadcast on</h4>
    </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,mirror_msg_filter];
}

function media_message_to_td(msg,user) {
 console.log('msg',msg);
 var media_attributes;
 try {
  media_attributes=JSON.parse(msg.replace('::{"','{"'));
 } catch(e) {
  return $('<td>').text(msg);
 }
 var msgtd;
 var omituser;
 media_filters().some(mf=>{
  var [handled,tmpmsgtd,tmpomituser]=mf(media_attributes,user);
  if(handled) {
   msgtd=tmpmsgtd;
   omituser=tmpomituser;
   return true;
  }
  return false;
 });
 return [msgtd,omituser];
}

function mirror_msg_filter(attr,user) {
 if(attr.type=='mirror') {
  if(user==window.user) {
   if($('#broadcast').css('display')=='none') {  //We will only do this once
    //Add broadcast control to sidebar
    var on=true;
    $('#broadcast').css('display','block').click(()=>{
     on=!on;
     $('#broadcast').text('Broadcast '+(on?'on':'off'));
    });
    //Listen for clicks that we need to broadcast relevent javascript for
    $('body').click((e)=>{
     console.log('broadcast click',e.target);
     //The two click events I want to watch for are embed clicks and close clicks
     var target=e.target;
     while(on && target!=document.body) {
      if(target.dataset.rebroadcast) {
       sendmessagetopeers('::'+JSON.stringify({'type':'ctrl_instr',run:target.dataset.rebroadcast}));
       break;  
      }
      target=target.parentElement;
     }
    });
   }
  }
  var td=$('<td>').css('cursor','pointer');
  var button=$('<button>').text(`Give ${user} control`);
  td.append(button);
  td.click(()=>{
   //Add user to white list for control
   window.whitelist = window.whitelist || new Set();
   if(user!=window.user && !window.whitelist.has(user)) {
    window.whitelist.add(user);
    var li = $('<li>').text(user).attr('title','Click to remove');
    $('#mirrorheader').css('display','block');
    $('#controllers').append(li);
    li.click(()=>{
     li.remove();
     window.whitelist.delete(user);
     if($('#controllers').children().length==0) {
      $('#mirrorheader').css('display','none');
     }
    });
   }
  });
  return [true,td];
 }
 else if(attr.type="ctrl_instr") {
  if(window.whitelist && window.whitelist.has(user)) {
   //We are trusting this.
   eval(attr.run);
  }
  return [true,null];
 }
}

function video_msg_filter(attr,user) {
 if(attr.type==='mp4') {
  console.log('embedbutton',attr,user);
  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.attr('data-rebroadcast',`display_mp4_in_chat('${attr.url}',${attr.sync?attr.start:null},${attr.loop})`);
  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') {
  console.log('A poll by',attr.user)
  if(user==window.user) {
   //This is me.
   window.mypolls = window.mypolls||new Set();
   window.mypolls.add(attr.id);
  }
  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','8px 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)
  if(!polltd[0]) {
   return [true,null]; //The poll does not exist
  }
  var votes = polltd[0].votes[attr.for];
  if(attr.nullify) {
   delete votes[user]; //They removed their vote
  }
  else {
   votes[user]=true; //They voted for something
  }
  polltd.find(`tr:nth-child(${attr.for+1}) td`).first().text(Object.keys(votes).length); //Update the cound for the option in ui
  if(window.mypolls && window.mypolls.has(attr.forid)&&user!=window.user) {
   //They voted on my poll, and they aren't me
   if(!(window.lasttovote && window.lasttovote==user)) {
    //We don't want a bunch of notifications about the same person
    var td=$('<td>').css('font-size','0.7em').text(user+' voted on your poll: '+polltd.find('h3').text().replace('Poll: ',''));
    window.lasttovote = user; //Store that they voted because we don't want a notice for every vote.
    return [true,td,true]; //We are going to send a notification; //We are not going to have the user td
   }
  }
  return [true,null];
 }
 else {
  return [false,null];
 }
}

function newestNPosts(posts,n) {
 var retr={};
 Object.keys(posts).sort((a,b)=>{
  return a-b;
 }).slice(-n).forEach(nonce=>{
  retr[nonce]=posts[nonce];
 });
 return retr;
}

function storepostsinpkey() {
 if(!window.storepkeylock) {
  window.storepkeylock = true;
  setTimeout(()=>{
   console.log('storing posts',window.rooomname);
   $.post('/pkey/set/wchat/'+window.roomname+'/posts',JSON.stringify(newestNPosts(window.posts,200)));
   delete window.storepkeylock;
  },1000*30);
 }
}

function getpostsfrompkey(room,cb,targetuser) {
 getjquery(()=>{
  $.post('/pkey/get/'+(targetuser||window.user)+'/wchat/'+room+'/posts',posts=>{
   if(!(posts && posts.length)) {
    posts='{}';
   }
   posts=JSON.parse(posts);
   if(cb) {
    cb(posts);
   }
   else {
    Object.values(posts).forEach(p=>{
     add_msg_to_ui(p.user,p.msg,p.nonce);
    });
   }
  });
 });
}

function add_msg_to_ui(user,msg,nonce) {
 var msgtd;
 var omituser;
 window.posts = window.posts || {};
 if(window.posts[nonce]) {
  return;
 }
 window.posts[nonce]={user,msg,nonce};
 if(window.allpostsgathered) {
  storepostsinpkey();
 }
 var bottom = testbottom();
 if(msg.startsWith('::{"')) {
  [msgtd,omituser] = media_message_to_td(msg,user);
 }
 else {
  msgtd = $('<td>').text(msg).addClass('msg');
  getmarked(()=>{
   msgtd.html(marked(msg));
   restorebottom(bottom);
  });
 }
 if(!msgtd) {
  return;
 }
 
 var table = $('.chat > table.content');
 var tr = $('<tr>').attr('data-user',user).attr('data-nonce',nonce);
 var usertext;
 if(omituser || table.find('tr').last().attr('data-user')==user) {
  usertext="";
 }
 else {
  usertext=user;
 }
 var userA = $('<a>');
 userA.attr('href','/u/'+user).attr('target','_blank').html(usertext);
 var usertd = $('<td>').append(userA).addClass('user');
 var t1 = table[0];
 //table.find('>tbody').append(tr.append(usertd).append(msgtd));
 tr.append(usertd).append(msgtd);
 var othertr = table.find('> tbody > tr');
 var aftertr=null
 for(var c=0;c<othertr.length;++c) {
  var othernonce = parseInt(othertr[c].getAttribute('data-nonce'));
  if(othernonce==nonce) {
    //This should never be possible.  It should have been stopped at window.posts[nonce];
    //In either case return because we aren't going to post it now.
    return;
  }
  if(othernonce>nonce) {
   aftertr=othertr[c];
   break;
  }
 }
 if(aftertr) {
  $(aftertr).before(tr);
 }
 else {
  console.log('aftertr not found')
  table.children().first().append(tr);
 }
 restorebottom(bottom);
}

function avatarurlfor(user,cb) {
 window.avatarmap = window.avatarmap || {};
 if(avatarmap[user]) {
  if(avatarmap[user]=='none') {
   return cb('');
  }
  return cb(avatarmap[user]);
 }
 $.post('/pkey/get/'+user+'/wchat/avatar',src=>{
  avatarmap[user]=src||'none';
  cb(src);
 });
}

function avatarfor(user) {
 var retr=$('<img>').addClass('avatar');
 avatarurlfor(user,src=>{
  retr.attr('src',src);
  if(src) {
   retr.css('border','1px solid gray');
   retr.css('visibility','visible');
  }
 });
 return retr;
}


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

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


function register_embed_form_listeners () {
 var dialogform = $('.dialogform');
 ['image','video','poll','mirror','avatar','upload'].forEach((i,idx,list)=>{
  $('#'+i+'typeradio').click(()=>{
   $('.dialogform').addClass('on'+i);
   list.forEach((i2,idx2)=>{
    if(idx==idx2) { return; }
    $('.dialogform').removeClass('on'+i2);
   });
  });
 });
 $('#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;
  }); 
 }
 if(thetype=='avatar') {
  $.post('/pkey/set/wchat/avatar',form.find('.url').val());
  return;
 }
 form.find('input').empty();
 ['image,video,poll,sync'].forEach(kind=>{
  form.removeClass('on'+kind);
 });
 retr.target='side';
 return retr;
}



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

function display_youtube_in_chat(videoId,start,loop) {
 var t1 = $('.chat > table.content')[0];
 var bottom = testbottom();
 var video=$('<div>').attr('id','yt'+Date.now());
 $('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');
 close.attr('data-rebroadcast','$(".fa-window-close").click()');
 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));
 restorebottom(bottom);
 grow.click(()=>{
  var bottom = testbottom();
  $('.container').css('max-width',parseInt($('.container').css('max-width'))-10+'%');
  var content=$('table.content')[0];
  $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
  restorebottom(bottom);
 });
 shrink.click(()=>{
  $('.container').css('max-width',parseInt($('.container').css('max-width'))+10+'%');
  $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
 });
 controls.find('i').mouseover(()=>{
  $('.side').addClass('hasinteracted');
 });
 var initialnow = Date.now();
 console.log('videoId',videoId);
 getytplayer(()=>{
  console.log('We have yt api');
  new YT.Player(video[0],{
   width:1280,
   height:720,
   videoId:videoId,
   playerVars:{autoplay:1},
   events:{
    onError:console.log,
    onStateChange:event=>{
     console.log('state change',event);
     if(loop&&event.data===0) {
      event.target.playVideo();
     }
    },
    onReady:event=>{
     var player=event.target;
     window.myplayer=player;
     $('body>.side>iframe').css('width','auto').css('display','block');
     restorebottom(bottom);
     close.click(()=>{
      controls.remove();
      $(player.a).remove();
      if($('body > .side').children().length===0) {
       $('body').removeClass('openside');
       $('body .container').css('max-width','');
       $('body > .side').removeClass('hasinteracted');
      }
      var content=$('table.content')[0];
      $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
     });
     console.log('We have player',player);
     var resynccount = 0;
     function resync () {
      ++resynccount;
      if(start>Date.now()) {
       player.pauseVideo();
       setTimeout(resync,start-Date.now());
       setTimeout(()=>{
        player.playVideo();
        initialnow = start;
       },start-Date.now());
       return;
      }
      var currentTime = ((Date.now()-start)%(loop?player.getDuration()*1000:Infinity))/1000;
      console.log('seeking player',currentTime);
      player.seekTo(currentTime);
      console.log('offset set',currentTime,start);
      setTimeout(resync,(Date.now()-initialnow)*resynccount);
     }
     if(start) {
      resync();
     }
    }
   }
  });
 });
}

function display_mp4_in_chat(mp4url,start,loop) {
 var possibleYoutubeId = isyoutubedomaingetvidid(mp4url);
 if(possibleYoutubeId) {
  return display_youtube_in_chat(possibleYoutubeId,start,loop);
 }
 var t1 = $('.chat > table.content')[0];
 var bottom = testbottom();
 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');
 close.attr('data-rebroadcast','$(".fa-window-close").click()');
 var grow = $('<i>').addClass('far').addClass('fa-plus-square');
 var shrink = $('<i>').addClass('far').addClass('fa-minus-square');
 var t1 = $('.chat > table.content')[0];
 $('body > .side').prepend(video);
 $('body > .side').prepend(controls.append(shrink).append(grow).append(close));
 restorebottom(bottom);
 close.click(()=>{
  controls.remove();
  video.remove();
  if($('body > .side').children().length===0) {
   $('body').removeClass('openside');
   $('body .container').css('max-width','');
   $('body > .side').removeClass('hasinteracted');
  }
  var content=$('table.content')[0];
  $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
 });
 grow.click(()=>{
  var bottom = testbottom();
  $('.container').css('max-width',parseInt($('.container').css('max-width'))-10+'%');
  var content=$('table.content')[0];
  $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
  restorebottom(bottom);
 });
 shrink.click(()=>{
  $('.container').css('max-width',parseInt($('.container').css('max-width'))+10+'%');
  $('#sidebar').css('display',content.scrollWidth>content.offsetWidth?'none':'flex');
 });
 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);
 }
}

function isyoutubedomaingetvidid(str) {
 var url=new URL(str);
 var against = new Set(['youtube.com','hooktube.com','invidio.us','youtu.be','youtube-nocookie.com','m.youtube.com','www.youtube.com','tube.poal.co']);
 var shortend = new Set(['youtu.be']); //https://youtu.be/PHgc8Q6qTjc
 if(shortend.has(url.host)) {
  return url.pathname.replace('/','');
 }
 else if(against.has(url.host)) {
  var match=url.search.match(/[?&]v=\w+/)
  if(!match) {
   return; //We don't have v={videoId}
  }
  return match[0].replace(/[?&]v=(\w+)/,'$1');
 }
}

Init

startChat();

Build

wchat