$ git clone http://socialnetwork.ion.nu/socialnetwork-web.git
commit effc5d880218b170d351f63cd8b5bf4924eab968
Author: Alicia <...>
Date:   Thu Aug 17 05:34:02 2017 +0000

    Implemented support for fields.

diff --git a/Makefile b/Makefile
index 2b9da73..8ff756a 100644
--- a/Makefile
+++ b/Makefile
@@ -4,11 +4,11 @@ EMSCRIPTVERSION=1.37.12
 GMPVERSION=6.1.2
 NETTLEVERSION=3.3
 GNUTLSVERSION=3.5.12
-SOCIALNETWORKREVISION=f5e92db17a617d4777df4b7a1107d67114d3b6f9
+SOCIALNETWORKREVISION=b3f54256b676bbca6805fd5c27d4270f6994f31d
 REVISION=$(shell git log | sed -n -e 's/^commit //p;q')
 JSLIBS=$(shell PKG_CONFIG_PATH=toolchain/usr/lib/pkgconfig pkg-config --libs gnutls nettle hogweed) -lgmp
 JSCFLAGS=$(shell PKG_CONFIG_PATH=toolchain/usr/lib/pkgconfig pkg-config --cflags gnutls)
-JSSYMBOLS='_social_init','_peer_handlesocket','_peer_new_unique','_websockproxy_read','_websockproxy_setwrite','_getcirclecount','_circle_getcount','_circle_getname','_circle_getprivacyptr','_social_addfriend','_circle_getid','_social_finduser','_self_getid','_user_getupdatecount','_user_getupdateptr','_update_gettype','_update_gettimestamp','_update_post_getmessage','_setcircle','_privacy_getflags','_privacy_getcirclecount','_privacy_getcircle','_createpost'
+JSSYMBOLS='_social_init','_peer_handlesocket','_peer_new_unique','_websockproxy_read','_websockproxy_setwrite','_getcirclecount','_circle_getcount','_circle_getname','_circle_getprivacyptr','_social_addfriend','_circle_getid','_social_finduser','_self_getid','_user_getupdatecount','_user_getupdateptr','_update_gettype','_update_gettimestamp','_update_getprivacy','_update_post_getmessage','_update_field_getname','_update_field_getvalue','_setcircle','_privacy_getflags','_privacy_getcirclecount','_privacy_getcircle','_createpost','_setfield'
 
 all: webpeer libsocial.js
 
diff --git a/jsglue.c b/jsglue.c
index 0e59ac7..37e06ef 100644
--- a/jsglue.c
+++ b/jsglue.c
@@ -94,7 +94,10 @@ const char* update_gettype(struct update* update)
   return "Unknown";
 }
 uint64_t update_gettimestamp(struct update* update){return update->timestamp;}
+struct privacy* update_getprivacy(struct update* update){return &update->privacy;}
 const char* update_post_getmessage(struct update* update){return update->post.message;}
+const char* update_field_getname(struct update* update){return update->field.name;}
+const char* update_field_getvalue(struct update* update){return update->field.value;}
 const char* self_getid(void)
 {
   unsigned char* bin=social_self->id;
@@ -114,3 +117,12 @@ void createpost(const char* msg, uint8_t flags, void* circles, uint32_t circleco
   };
   social_createpost(msg, &priv);
 }
+void setfield(const char* name, const char* value, uint8_t flags, void* circles, uint32_t circlecount)
+{
+  struct privacy priv={
+    .flags=flags,
+    .circles=circles,
+    .circlecount=circlecount
+  };
+  social_updatefield(name, value, &priv);
+}
diff --git a/libsocialjs.js b/libsocialjs.js
index c934ef4..e0bd206 100644
--- a/libsocialjs.js
+++ b/libsocialjs.js
@@ -37,12 +37,16 @@ var user_getupdatecount;
 var user_getupdateptr;
 var update_gettype;
 var update_gettimestamp;
+var update_getprivacy;
 var update_post_getmessage;
+var update_field_getname;
+var update_field_getvalue;
 var self_getid;
 var privacy_getflags;
 var privacy_getcirclecount;
 var privacy_getcircle;
 var createpost;
+var setfield;
 
 var websockproxy_to=false;
 var firstpacket=true;
@@ -75,8 +79,8 @@ function handlenet(data)
 }
 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']);
+  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', []);
   circle_getcount=Module.cwrap('circle_getcount', 'number', ['number']);
@@ -90,12 +94,16 @@ function init(privkey)
   user_getupdateptr=Module.cwrap('user_getupdateptr', 'number', ['number','number']);
   update_gettype=Module.cwrap('update_gettype', 'string', ['number']);
   update_gettimestamp=Module.cwrap('update_gettimestamp', 'number', ['number']);
+  update_getprivacy=Module.cwrap('update_getprivacy', 'number', ['number']);
   update_post_getmessage=Module.cwrap('update_post_getmessage', 'string', ['number']);
+  update_field_getname=Module.cwrap('update_field_getname', 'string', ['number']);
+  update_field_getvalue=Module.cwrap('update_field_getvalue', 'string', ['number']);
   self_getid=Module.cwrap('self_getid', 'string', []);
   privacy_getflags=Module.cwrap('privacy_getflags', 'number', ['number']);
   privacy_getcirclecount=Module.cwrap('privacy_getcirclecount', 'number', ['number']);
-  privacy_getcircle=Module.cwrap('privacy_getcircle', 'number', ['number', 'number']);
-  createpost=Module.cwrap('createpost', null, ['string', 'number', 'array', 'number']);
+  privacy_getcircle=Module.cwrap('privacy_getcircle', 'number', ['number','number']);
+  createpost=Module.cwrap('createpost', null, ['string','number','array','number']);
+  setfield=Module.cwrap('setfield', null, ['string','string','number','array','number']);
 
   _websockproxy_setwrite(Runtime.addFunction(websockwrite));
   FS.writeFile('privkey.pem', privkey, {});
@@ -160,8 +168,17 @@ function getuser(id)
   var user=new Object();
   user.ptr=userptr;
   user.id=id;
-// TODO: Gather more user data? No need to store updates themselves in user objects though
+// TODO: Gather more user data? No need to store updates themselves in user objects though, except for fields
   user.updatecount=user_getupdatecount(userptr);
+  user.fields={};
+  for(var i=0; i<user.updatecount; ++i)
+  {
+    var update=user_getupdate(user, i);
+    if(update.type=='Field')
+    {
+      user.fields[update.name]=update;
+    }
+  }
   return user;
 }
 function user_getupdate(user, index)
@@ -170,12 +187,17 @@ function user_getupdate(user, index)
   var ptr=user_getupdateptr(user.ptr, index);
   update.type=update_gettype(ptr);
   update.timestamp=update_gettimestamp(ptr);
+  update.privacy=getprivacy(update_getprivacy(ptr));
   // Get type-specific data
   switch(update.type)
   {
   case 'Post':
     update.message=update_post_getmessage(ptr);
     break;
+  case 'Field':
+    update.name=update_field_getname(ptr);
+    update.value=update_field_getvalue(ptr);
+    break;
   }
   return update;
 }
diff --git a/websocial.css b/websocial.css
index d2b6278..4105a9e 100644
--- a/websocial.css
+++ b/websocial.css
@@ -62,3 +62,17 @@ div.update
   box-shadow:0px 0px 2px #c0c0c0;
   background-color:#ffffff;
 }
+div.updatebox
+{
+  border-style:solid;
+  border-color:#000000;
+  border-width:1px;
+  border-radius:6px;
+  padding:4px;
+  margin:5px;
+  min-width:40%;
+  max-width:96%;
+  display:inline-block;
+  box-shadow:0px 0px 4px #0080ff;
+  background-color:#ffffff;
+}
diff --git a/websocial.js b/websocial.js
index 795a7ad..8360a8f 100644
--- a/websocial.js
+++ b/websocial.js
@@ -135,8 +135,19 @@ function page_friends()
     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));
+      var id=circle_getid(item.index, i);
+      // Get user and get their name field, TODO: maybe a 'picture' field for main profile picture?
+      var user=getuser(id);
+      if(!user){continue;}
+      var name=(user.fields['name']?user.fields['name'].value:id);
+      if(!name){name=id;}
+      // Link to profile
+      var link=document.createElement('a');
+      link.href='#';
+      link.dataset.id=id;
+      link.onclick=function(){page_user(this.dataset.id); return false;};
+      link.appendChild(document.createTextNode(name));
+      display.appendChild(link);
       display.appendChild(document.createElement('br'));
     }
   }
@@ -144,19 +155,54 @@ function page_friends()
 
 function page_user(id)
 {
-  if(!id){id=self_getid();} // No ID = show our own page
+  var userself=self_getid();
+  if(!id){id=userself;} // No ID = show our own page
   var display=document.getElementById('display');
   dom_clear(display);
-  // TODO: Get and present fields
   var user=getuser(id);
-  display.appendChild(document.createTextNode(id+':'));
-  display.appendChild(document.createElement('br'));
-  var button=document.createElement('button');
-  button.name='postbutton';
-  button.onclick=postwidget;
-  button.appendChild(document.createTextNode('Create update'));
-  display.appendChild(button);
-  display.appendChild(document.createElement('br'));
+  // Placeholder for common fields
+  if(!user.fields['name']){user.fields['name']={'value':''};}
+  // List fields and their values
+  var fields=document.createElement('p');
+  for(field in user.fields)
+  {
+    var span=document.createElement('span');
+    span.appendChild(document.createTextNode(field+': '+user.fields[field].value));
+    if(id==userself) // If self, enable editing
+    {
+      span.dataset.name=field;
+      span.dataset.value=user.fields[field].value;
+      if(user.fields[field].privacy)
+      {
+        span.dataset.privacyflags=user.fields[field].privacy.flags;
+        span.dataset.privacycircles=JSON.stringify(user.fields[field].privacy.circles);
+      }
+      span.className='updatebutton';
+      span.onclick=fieldwidget;
+      var button=document.createElement('button');
+      button.appendChild(document.createTextNode('Edit'));
+      span.appendChild(button);
+    }
+    fields.appendChild(span);
+    fields.appendChild(document.createElement('br'));
+  }
+  if(id==userself) // If self, enable editing
+  {
+    // Field button
+    var button=document.createElement('button');
+    button.className='updatebutton';
+    button.onclick=fieldwidget;
+    button.appendChild(document.createTextNode('Add field'));
+    fields.appendChild(button);
+    display.appendChild(fields);
+    // Post button
+    button=document.createElement('button');
+    button.className='updatebutton';
+    button.onclick=postwidget;
+    button.appendChild(document.createTextNode('Create post'));
+    display.appendChild(button);
+    display.appendChild(document.createElement('br'));
+  }
   display.appendChild(document.createTextNode(user.updatecount+' updates'));
   for(var i=1; i<=user.updatecount && i<=20; ++i)
   {
@@ -272,55 +318,94 @@ function circle_save()
   chdisplay('circle_window',false);
 }
 
-var postwidgetbox=false;
-var postprivacy=new privacy(0,[]);
-function postwidget()
+var updatewidgetbox=false;
+var updateprivacy=new privacy(0,[]);
+function updatewidget(updatebutton, title)
 {
-// TODO: Differences in widgets for posts on others walls or in response to other posts?
-  if(postwidgetbox && postwidgetbox.parentNode)
+  if(updatewidgetbox && updatewidgetbox.parentNode)
   {
-    postwidgetbox.parentNode.removeChild(postwidgetbox);
+    updatewidgetbox.parentNode.removeChild(updatewidgetbox);
   }
-  postwidgetbox=document.createElement('div');
+  updatewidgetbox=document.createElement('div');
+  updatewidgetbox.className='updatebox';
 // TODO: Configurable default privacy?
-  var privtext=document.createTextNode('Privacy: '+postprivacy.toString());
-  postwidgetbox.appendChild(privtext);
+  var bold=document.createElement('strong');
+  bold.appendChild(document.createTextNode(title));
+  updatewidgetbox.appendChild(bold);
+  updatewidgetbox.appendChild(document.createElement('br'));
+  var privtext=document.createTextNode('Privacy: '+updateprivacy.toString());
+  updatewidgetbox.appendChild(privtext);
   button=document.createElement('button');
   button.appendChild(document.createTextNode('Change'));
   button.onclick=function()
   {
-    privacy_openconfig('generic', postprivacy);
+    privacy_openconfig('generic', updateprivacy);
     document.getElementById('privacy_button').onclick=function()
     {
-      postprivacy=privacy_save('generic');
+      updateprivacy=privacy_save('generic');
       chdisplay('privacy_window', false);
-      privtext.textContent='Privacy: '+postprivacy.toString();
+      privtext.textContent='Privacy: '+updateprivacy.toString();
     };
     chdisplay(false, 'privacy_window');
   };
-  postwidgetbox.appendChild(button);
-  postwidgetbox.appendChild(document.createElement('br'));
+  updatewidgetbox.appendChild(button);
+  updatewidgetbox.appendChild(document.createElement('br'));
+  // Re-show all updatebuttons
+  var buttons=document.getElementsByClassName('updatebutton');
+  for(button of buttons)
+  {
+    button.style.display='inline';
+  }
+  // Hide this one and insert updatewidget
+  updatebutton.style.display='none';
+  updatebutton.parentNode.insertBefore(updatewidgetbox, updatebutton);
+  return updatewidgetbox;
+}
+function postwidget()
+{
+// TODO: Differences in widgets for posts on others walls or in response to other posts?
+  var box=updatewidget(this, 'New post');
 // TODO: Non-text posts, maybe media for starters
   var text=document.createElement('textarea');
-  postwidgetbox.appendChild(text);
-  postwidgetbox.appendChild(document.createElement('br'));
+  box.appendChild(text);
+  box.appendChild(document.createElement('br'));
   // Submit button
   button=document.createElement('button');
   button.appendChild(document.createTextNode('Post'));
   button.onclick=function()
   {
-    createpost(text.value, postprivacy.flags, postprivacy.bincircles(), postprivacy.circles.length);
-// TODO: Instead of reloading the user page, insert the update and hide the postwidget (and re-show postbutton)
+    createpost(text.value, updateprivacy.flags, updateprivacy.bincircles(), updateprivacy.circles.length);
+// TODO: Instead of reloading the user page, insert the update and hide the updatewidget (and re-show postbutton)
     page_user(false);
   };
-  postwidgetbox.appendChild(button);
-  // Re-show all postbuttons
-  var buttons=document.getElementsByName('postbutton');
-  for(button of buttons)
+  box.appendChild(button);
+}
+function fieldwidget()
+{
+  if(this.dataset.privacyflags!==undefined && this.dataset.privacycircles)
   {
-    button.style.display='inline';
+    updateprivacy.flags=this.dataset.privacyflags;
+    updateprivacy.circles=JSON.parse(this.dataset.privacycircles);
   }
-  // Hide this one and insert postwidget
-  this.style.display='none';
-  this.parentNode.insertBefore(postwidgetbox, this);
+  var box=updatewidget(this, this.dataset.name?'Edit field':'New field');
+  var name=document.createElement('input');
+  name.type='text';
+  box.appendChild(name);
+  box.appendChild(document.createTextNode(': '));
+  var value=document.createElement('input');
+  value.type='text';
+  // Use existing values if provided
+  if(this.dataset.name){name.value=this.dataset.name; name.readOnly=true;}
+  if(this.dataset.value){value.value=this.dataset.value;}
+  box.appendChild(value);
+  // Save button
+  button=document.createElement('button');
+  button.appendChild(document.createTextNode('Save'));
+  button.onclick=function()
+  {
+    setfield(name.value, value.value, updateprivacy.flags, updateprivacy.bincircles(), updateprivacy.circles.length);
+// TODO: Instead of reloading the user page, insert the field and hide the updatewidget (and re-show field button)
+    page_user(false);
+  };
+  box.appendChild(button);
 }