$ git clone https://socialnetwork.ion.nu/socialnetwork-web.git
commit 3360442d618ce8968f987d6506a7c5a2485c63ff
Author: Alicia <...>
Date:   Sat Jul 15 20:46:12 2017 +0200

    Initial friends list implementation.

diff --git a/Makefile b/Makefile
index b2787f2..1371525 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ toolchain/usr/lib/libsocial.so: toolchain/usr/bin/emcc
  ./buildlibs.sh '$(GMPVERSION)' '$(NETTLEVERSION)' '$(GNUTLSVERSION)'
 
 libsocial.js: jsglue.c toolchain/usr/lib/libsocial.so
- toolchain/usr/bin/emcc -O3 -s EXPORTED_FUNCTIONS="['_social_init','_peer_handlesocket','_peer_new_unique','_websockproxy_read','_websockproxy_setwrite','_jsglue_addfile']" -s RESERVED_FUNCTION_POINTERS=1 -s NO_EXIT_RUNTIME=1 -s NODEJS_CATCH_EXIT=0 -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','readFile','writeFile']" -s INVOKE_RUN=0 $(JSCFLAGS) $^ $(JSLIBS) -o $@
+ toolchain/usr/bin/emcc -O3 -s EXPORTED_FUNCTIONS="['_social_init','_peer_handlesocket','_peer_new_unique','_websockproxy_read','_websockproxy_setwrite','_getcirclecount','_newcircle','_circle_getcount','_circle_getname','_circle_setname','_social_addfriend','_circle_getid']" -s RESERVED_FUNCTION_POINTERS=1 -s NO_EXIT_RUNTIME=1 -s NODEJS_CATCH_EXIT=0 -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','readFile','writeFile']" -s INVOKE_RUN=0 $(JSCFLAGS) $^ $(JSLIBS) -o $@
  ./squeezeandcomment.sh '$@' 'This code was built from gmp $(GMPVERSION), nettle $(NETTLEVERSION), gnutls $(GNUTLSVERSION) and socialnetwork git revision $(SOCIALNETWORKREVISION) with tweaks from socialnetwork-web git revision $(REVISION) using emscripten (fastcomp) $(EMSCRIPTVERSION)'
 
 # Download all external sources we'll need
diff --git a/jsglue.c b/jsglue.c
index d5ae414..858f6af 100644
--- a/jsglue.c
+++ b/jsglue.c
@@ -16,11 +16,13 @@
 */
 #include <stdlib.h>
 #include <string.h>
+#include <stdio.h>
 #include <stdint.h>
 #include <time.h>
 #include <sys/socket.h>
 #include <libsocial/udpstream_private.h>
 #include <libsocial/udpstream.h>
+#include <libsocial/social.h>
 #include "jsglue.h"
 extern struct udpstream* stream_new(int sock, struct sockaddr_storage* addr, socklen_t addrlen);
 
@@ -42,3 +44,54 @@ void websockproxy_read(struct sockaddr_storage* addr, socklen_t addrlen, const v
   stream->recvpackets[stream->recvpacketcount-1].buflen=payloadsize;
   memcpy(stream->recvpackets[stream->recvpacketcount-1].buf, buf, payloadsize);
 }
+
+unsigned int getcirclecount(void)
+{
+  return social_self->circlecount;
+}
+
+// Find an empty circle and reset it, empty and nameless I guess, then return its index
+unsigned int newcircle(void)
+{
+  unsigned int i;
+  for(i=0; i<social_self->circlecount; ++i)
+  {
+    if(!social_self->circles[i].count && !social_self->circles[i].name){break;}
+  }
+  if(i==social_self->circlecount)
+  {
+    ++social_self->circlecount;
+    social_self->circles=realloc(social_self->circles, social_self->circlecount*sizeof(struct friendslist));
+  }else{
+    free(social_self->circles[i].privacy.circles);
+  }
+  memset(&social_self->circles[i], 0, sizeof(struct friendslist));
+  return i;
+}
+// TODO: how to configure privacy structs?
+
+unsigned int circle_getcount(unsigned int i)
+{
+  if(i>=social_self->circlecount){return 0;}
+  return social_self->circles[i].count;
+}
+const char* circle_getid(unsigned int i, unsigned int u)
+{
+  if(i>=social_self->circlecount){return 0;}
+  if(u>=social_self->circles[i].count){return 0;}
+  unsigned char* bin=social_self->circles[i].friends[u]->id;
+  static char id[ID_SIZE*2+1];
+  sprintf(id, PEERFMT, PEERARG(social_self->circles[i].friends[u]->id));
+  return id;
+}
+const char* circle_getname(unsigned int i)
+{
+  if(i>=social_self->circlecount){return 0;}
+  return social_self->circles[i].name;
+}
+void circle_setname(unsigned int i, const char* name)
+{
+  if(i>=social_self->circlecount){return;}
+  free(social_self->circles[i].name);
+  social_self->circles[i].name=strdup(name);
+}
diff --git a/libsocialjs.js b/libsocialjs.js
index 557afed..77021c5 100644
--- a/libsocialjs.js
+++ b/libsocialjs.js
@@ -25,6 +25,10 @@ function websockwrite(addr, addrlen, buf, size)
 
 var websockproxy_read;
 var peer_new_unique;
+var circle_getname;
+var circle_setname;
+var circle_getid;
+var social_addfriend;
 
 var websockproxy_to=false;
 var firstpacket=true;
@@ -59,6 +63,15 @@ function init(privkey)
 {
   websockproxy_read=Module.cwrap('websockproxy_read', null, ['array', 'number', 'array', 'number']);
   peer_new_unique=Module.cwrap('peer_new_unique', 'array', ['number', 'array', 'number']);
+  // Low level access functions
+  getcirclecount=Module.cwrap('getcirclecount', 'number', []);
+  newcircle=Module.cwrap('newcircle', 'number', []);
+  circle_getcount=Module.cwrap('circle_getcount', 'number', ['number']);
+  circle_getname=Module.cwrap('circle_getname', 'string', ['number']);
+  circle_setname=Module.cwrap('circle_setname', null, ['number', 'string']);
+  circle_getid=Module.cwrap('circle_getid', 'string', ['number','number']);
+  social_addfriend=Module.cwrap('social_addfriend', null, ['array', 'number']);
+
   _websockproxy_setwrite(Runtime.addFunction(websockwrite));
   FS.writeFile('privkey.pem', privkey, {});
   Module.ccall('social_init', null, ['string', 'string'], ['privkey.pem', '']);
@@ -66,3 +79,36 @@ function init(privkey)
   connection.onmessage=handlenet;
   connection.onclose=function(){alert('The connection to the server was lost.');};
 }
+function getcircles()
+{
+  var circles=new Array();
+  var len=getcirclecount();
+  for(var i=0; i<len; ++i)
+  {
+    var circle=new Object();
+    circle.name=circle_getname(i);
+    circle.index=i;
+    if(circle_getcount(i)>0 || circle.name!=''){circles.push(circle);}
+  }
+  return circles;
+}
+function circle_getfriends(index)
+{
+  // Call C functions to gather number of friends and their IDs
+  var friends=new Array();
+  var count=circle_getcount(index);
+  for(var i=0; i<count; ++i)
+  {
+    friends.push(circle_getid(index, i));
+  }
+  return friends;
+}
+function hextobin(hex)
+{
+  var bin=new Array();
+  for(var i=0; i<hex.length; i+=2)
+  {
+    bin.push(parseInt(hex.substring(i,i+2), 16));
+  }
+  return new Uint8Array(bin);
+}
diff --git a/squeezeandcomment.sh b/squeezeandcomment.sh
index ae4498f..a820cd7 100755
--- a/squeezeandcomment.sh
+++ b/squeezeandcomment.sh
@@ -4,7 +4,6 @@ cp "$file" "${file}.unsqueezed"
 comment="$2"
 base="`basename "$file"`"
 sed -i -e 's|// .*||; /^$/d' "$file" # Probably safe
-sed -i -e 's/throw new[^};]*;\?/;/g' "$file" # Don't waste space on error messages
 tr -d '\r\n' < "$file" > ".tmp.${base}" # Not so safe
 mv ".tmp.${base}" "$file"
 sed -i -e "1i// ${comment}" "$file" # 100% safe
diff --git a/websocial.html b/websocial.html
index e2f2e9f..fc59b39 100644
--- a/websocial.html
+++ b/websocial.html
@@ -43,9 +43,19 @@
       Note: generating a key takes a while, you may need to tell your browser to let the script continue.<br />
     </div>
   </div>
+  <div class="modal" id="circle_window" style="display:none;">
+    Name:<input type="text" id="circle_name" /><br />
+    Privacy:<select id="circle_privacy">
+      <option>Select circles only</option>
+      <option>Friends</option>
+      <option>Everyone</option>
+    </select><br />
+    <div id="circle_circles"></div><br />
+    <button onclick="circle_save();">OK</button> &nbsp; <button onclick="chdisplay('circle_window',false);">Cancel</button>
+  </div>
   <!-- Main interface -->
   <div class="menu">
-    <a href="#">Feed</a><a href="#">Friends</a><a href="#">Self</a>
+    <a href="#" onclick="return false;">Feed</a><a href="#" onclick="page_friends();return false;">Friends</a><a href="#" onclick="return false;">Self</a>
   </div>
   <div id="display"></div>
 </body>
diff --git a/websocial.js b/websocial.js
index f39cbcf..035fc43 100644
--- a/websocial.js
+++ b/websocial.js
@@ -4,10 +4,18 @@ function chdisplay(old,newview)
   if(newview){document.getElementById(newview).style.display='block';}
 }
 
+function dom_clear(element)
+{
+  while(element.childNodes.length>0)
+  {
+    element.removeChild(element.childNodes[0]);
+  }
+}
+
 function initgui()
 {
   var key=document.getElementById('privkey').value;
-// TODO: some basic checks that 'key' at least looks like it might be a key, otherwise set to '' so we get the key window
+// TODO: some basic checks that 'key' at least looks like it might be a key, otherwise set to '' so we still get the key window
   chdisplay('login_window',(key=='')?'key_window':false);
   // Delay the actual work a tiny bit to make sure the "have patience" message is rendered first
   setTimeout(function()
@@ -42,6 +50,110 @@ function initgui()
     button.onclick=function(){chdisplay('key_window', false);};
     button.style.float='right';
     w.appendChild(button);
-  }, 0);
+  }, 100);
 // TODO: display any updates we may have (actually we probably won't have any yet, update when we get them, callback for 'updates' command?)
 }
+
+function page_friends()
+{
+  var display=document.getElementById('display');
+  dom_clear(display);
+  var box=document.createElement('div');
+  box.style.width='intrinsic';
+  box.style.width='-webkit-max-content';
+  box.style.width='-moz-max-content';
+  box.style.width='max-content';
+  box.style.marginLeft='auto';
+  box.style.marginRight='auto';
+  var entry=document.createElement('input');
+  entry.placeholder='SocialNetwork ID';
+  entry.size=64;
+  box.appendChild(entry);
+  var circles=getcircles();
+  var circle=document.createElement('select');
+  // List existing circles
+  for(item of circles)
+  {
+    var option=document.createElement('option');
+    option.value=item.index;
+    option.appendChild(document.createTextNode(item.name));
+    circle.appendChild(option);
+  }
+  var option=document.createElement('option');
+  option.value='new';
+  option.text='New circle';
+  circle.appendChild(option);
+  circle.onchange=function()
+  {
+    if(this.value=='new')
+    {
+      configcircle_option=this.selectedOptions[0];
+      circle_openconfig(newcircle());
+    }
+  }
+  box.appendChild(circle);
+  var button=document.createElement('button');
+  button.appendChild(document.createTextNode('Add friend'));
+  button.onclick=function()
+  {
+    if(circle.value=='new') // Need to create the circle first
+    {
+      configcircle_option=circle.selectedOptions[0]; // option;
+      circle.onchange();
+      return;
+    }
+    var id=hextobin(entry.value);
+    social_addfriend(id, circle.value);
+    page_friends();
+  };
+  box.appendChild(button);
+  display.appendChild(box);
+  // TODO: Make this prettier, links to individual profile pages and get the name property
+  for(item of circles)
+  {
+    display.appendChild(document.createTextNode('Circle '+item.name+':'));
+    display.appendChild(document.createElement('br'));
+    var count=circle_getcount(item.index);
+    for(var i=0; i<count; ++i)
+    {
+      var user=circle_getid(item.index, i);
+      display.appendChild(document.createTextNode(user));
+      display.appendChild(document.createElement('br'));
+    }
+  }
+}
+
+var configcircle_index;
+var configcircle_option=false;
+function circle_openconfig(index)
+{
+  // TODO: Move circle-list population to its own function? likely needed for other privacy settings
+  var circles=document.getElementById('circle_circles');
+  dom_clear(circles);
+  // TODO: Populate with circles from libsocial
+  // TODO: Store index and checkbox elements for circle_save()
+  var name=document.getElementById('circle_name');
+  name.value=circle_getname(index);
+  configcircle_index=index;
+  chdisplay(false,'circle_window');
+}
+
+function circle_save()
+{
+  var name=document.getElementById('circle_name');
+  circle_setname(configcircle_index, name.value);
+  if(configcircle_option)
+  {
+    var select=configcircle_option.parentNode;
+    // Update list of circles
+    configcircle_option.text=name.value;
+    configcircle_option.value=configcircle_index;
+    configcircle_option=false;
+    // Make a new 'new circle' option?
+    var option=document.createElement('option');
+    option.value='new';
+    option.text='New circle';
+    select.appendChild(option);
+  }
+  chdisplay('circle_window',false);
+}