$ git clone http://thingshare.ion.nu/thingshare.git
commit 48351f737c99031fe1f0e0122d87884615ec62e1
Author: Alicia <...>
Date:   Tue Feb 2 19:55:32 2021 +0100

    "web2.0" asynchronous form submission for comments.

diff --git a/Dependencies b/Dependencies
index 53a3d52..2042b6d 100644
--- a/Dependencies
+++ b/Dependencies
@@ -3,7 +3,7 @@ Parsedown (git clone https://github.com/erusev/parsedown)
 Mdjs (git clone https://github.com/hangxingliu/mdjs)
 x3dom (wget https://x3dom.org/download/1.8.1/x3dom.{debug.js,css})
 
-Web dependencies:
+Webserver dependencies:
 PHP
 PHP curl module
 PHP gd module
diff --git a/docs/RPCs b/docs/RPCs
index adeac25..fa56ebc 100644
--- a/docs/RPCs
+++ b/docs/RPCs
@@ -115,10 +115,10 @@ Signed request format:
 {
   "from": <Sender's username>,
   "message": <Comment message, markdown>,
-  "replyto": <ID of parent comment.0 for a top comment>
+  "replyto": <ID of parent comment. 0 for a top comment>
 }
 Return format:
-{"status":"OK"} or {"error":<Error message>}
+{"status":"OK","id":<comment ID>} or {"error":<Error message>}
 
 RPC: messages/<Username>
 Sends a direct message to the user of the given username on this node.
diff --git a/head.php b/head.php
index bf80bc6..93f2247 100644
--- a/head.php
+++ b/head.php
@@ -110,6 +110,7 @@ if($path[1]=='tag')
   <script src="<?=BASEURL?>/x3dom.debug.js"></script>
   <script src="<?=BASEURL?>/3dview.js"></script>
   <script src="<?=BASEURL?>/time.js"></script>
+  <script src="<?=BASEURL?>/web2.0.js"></script>
 </head>
 <body>
   <div id="logo" title="Yes this needs a logo or something. Got any art skills?"></div>
@@ -128,7 +129,7 @@ if($path[1]=='tag')
         </div>
       </div>
       <!-- Link to user settings, messages -->
-      <a href="<?=BASEURL?>/user/<...>">My profile</a>
+      <a href="<?=BASEURL?>/user/<...>" id="profilelink"><?=$_SESSION['displayname']?></a>
       <a href="<?=BASEURL?>/messages"<?=$unreadmsgs?>>Messages</a>
       <a href="<?=$logoutlink?>">Log out</a>
     <?php }else{ ?>
diff --git a/login.php b/login.php
index 054d4a9..610aa7e 100644
--- a/login.php
+++ b/login.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
@@ -37,7 +37,7 @@ if(isset($_POST['user']) && isset($_POST['pass']))
     // Check password
     $error=_('Incorrect username/password');
     $user=mysqli_real_escape_string($db, $_POST['user']);
-    $res=mysqli_query($db, 'select salt, password, id, status from users where name="'.$user.'"');
+    $res=mysqli_query($db, 'select salt, password, id, status, displayname from users where name="'.$user.'"');
     if($res=mysqli_fetch_assoc($res))
     {
       $hash=explode(':', $res['password']);
@@ -50,7 +50,8 @@ if(isset($_POST['user']) && isset($_POST['pass']))
             session_start();
             $_SESSION['name']=$_POST['user'];
             $_SESSION['id']=$res['id'];
-            header('Location: '.(isset($_GET['returnto'])?urldecode($_GET['returnto']):BASEURL));
+            $_SESSION['displayname']=$res['displayname'];
+            header('Location: '.(isset($_GET['returnto'])?urldecode($_GET['returnto']):BASEURL.'/'));
             exit();
           case ACCOUNT_BANNED: $error=_('Banned'); break;
           case ACCOUNT_EMAILUNVERIFIED: $error=_('Please check for a verification e-mail'); break;
diff --git a/nonce.php b/nonce.php
index 24f1573..a5d3201 100644
--- a/nonce.php
+++ b/nonce.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
@@ -27,7 +27,10 @@ function checknonce()
 {
   $nonce=explode(':', $_POST['nonce']);
   $res=(isset($_SESSION['nonce'.$nonce[0]]) && $_SESSION['nonce'.$nonce[0]]==$nonce[1]);
-  unset($_SESSION['nonce'.$nonce[0]]);
+  if(!isset($_POST['asyncrequest']))
+  {
+    unset($_SESSION['nonce'.$nonce[0]]);
+  }
   return $res;
 }
 ?>
diff --git a/rpc_comments.php b/rpc_comments.php
index 283d8ec..b7e9caa 100644
--- a/rpc_comments.php
+++ b/rpc_comments.php
@@ -53,7 +53,7 @@ if(isset($_POST['data'])) // Posting a comment
   $q.='values('.$thing.', "'.$sender.'", '.$replyto.', "'.$msg.'", "'.$timestamp.'", false)';
   if(mysqli_query($db, $q))
   {
-    print('{"status":"OK"}');
+    print('{"status":"OK","id":'.mysqli_insert_id($db).'}');
     // Notify the parent commenter or thing's owner
     $link='/thing/'.$thing.'@'.DOMAIN.'#comment'.mysqli_insert_id($db);
     $name=getdisplayname($obj['from'].'@'.$peer);
diff --git a/start.php b/start.php
index 768f73d..53b66ef 100644
--- a/start.php
+++ b/start.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
@@ -36,7 +36,7 @@ function getcount($table)
   $res=mysqli_fetch_row($res);
   return $res[0];
 }
-$numusers=getcount('users where status!='.ACCOUNT_BANNED);
+$numusers=getcount('users where status='.ACCOUNT_ACTIVE);
 $numthings=getcount('things where latest and !removed');
 $numpeers=getcount('peers where !blacklist');
 ?>
diff --git a/thing.php b/thing.php
index 11c0542..d6676f8 100644
--- a/thing.php
+++ b/thing.php
@@ -2,7 +2,7 @@
 /*
     This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
     https://thingshare.ion.nu/
-    Copyright (C) 2020  Alicia <...>
+    Copyright (C) 2020-2021  Alicia <...>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
@@ -17,11 +17,46 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
-include_once('head.php');
+if(isset($_POST['asyncrequest']))
+{
+  session_start();
+}else{
+  include_once('head.php');
+}
 include_once('db.php');
 include_once('nonce.php');
 include_once('rpc.php');
+
 $thing=explode('@',$path[2]);
+if(isset($_SESSION['id']))
+{
+  // Check if we're blocking the user (can't comment on things of people you're blocking)
+  $by_esc=mysqli_real_escape_string($db, $thingobj['by']['name']);
+  $res=mysqli_query($db, 'select user from userblocks where user='.(int)$_SESSION['id'].' and blocked="'.$by_esc.'" limit 1');
+  $blocked=mysqli_fetch_row($res);
+  $info='';
+  $error='';
+  if(isset($_POST['msg']) && checknonce())
+  {
+    if($blocked){$error=_('Cannot send messages to blocked users');}else{
+      // Send it to thing's node
+      $msg=Array('from'=>$_SESSION['name'],
+                 'message'=>$_POST['msg'],
+                 'replyto'=>$_POST['replyto']);
+      $data=rpc_post($thing[1], 'comments/'.$thing[0], $msg);
+      if(isset($data['error']))
+      {
+        $error=$data['error'];
+        if(isset($_POST['asyncrequest'])){exit($error);}
+      }else{
+        rpc_cache($thing[1], 'comments/'.$thing[0], false); // Invalidate cache to show new comments
+        if(isset($_POST['asyncrequest'])){exit('ok:'.$data['id']);}
+        $info=_('Comment posted');
+      }
+    }
+  }
+}
+if(isset($_POST['asyncrequest'])){exit();}
 $thingobj=rpc_get($thing[1], 'thing/'.$thing[0]);
 if(isset($thingobj['error']))
 {
@@ -66,29 +101,6 @@ foreach($thingobj['tags'] as $tag)
   $tag=htmlentities($tag);
   $tags.=' <a href="'.BASEURL.'/tag/'.$tag.'">'.$tag.'</a>';
 }
-// Check if we're blocking the user (can't comment on things of people you're blocking)
-$by_esc=mysqli_real_escape_string($db, $thingobj['by']['name']);
-$res=mysqli_query($db, 'select user from userblocks where user='.(int)$_SESSION['id'].' and blocked="'.$by_esc.'" limit 1');
-$blocked=mysqli_fetch_row($res);
-$info='';
-$error='';
-if(isset($_POST['msg']) && checknonce())
-{
-  if($blocked){$error=_('Cannot send messages to blocked users');}else{
-    // Send it to thing's node
-    $msg=Array('from'=>$_SESSION['name'],
-               'message'=>$_POST['msg'],
-               'replyto'=>$_POST['replyto']);
-    $data=rpc_post($thing[1], 'comments/'.$thing[0], $msg);
-    if(isset($data['error']))
-    {
-      $error=$data['error'];
-    }else{
-      rpc_cache($thing[1], 'comments/'.$thing[0], false); // Invalidate cache to show new comments
-      $info=_('Comment posted');
-    }
-  }
-}
 $comments=rpc_get($thing[1], 'comments/'.$thing[0]);
 include_once('parsedown/Parsedown.php');
 $md=new Parsedown();
@@ -151,7 +163,7 @@ function replyto(comment)
 <?=printcomments($comments)?>
 <?php if(isset($_SESSION['id'])){ ?>
 <script src="<?=BASEURL?>/mdjs/mdjs.js"></script>
-<form method="post" action="<?=BASEURL?>/thing/<?=$path[2]?>">
+<form method="post" action="<?=BASEURL?>/thing/<?=$path[2]?>" id="commentform">
   <p>
     <?=nonce()?>
     <?php if(!$blocked){ ?>
diff --git a/web2.0.js b/web2.0.js
new file mode 100644
index 0000000..b0035d0
--- /dev/null
+++ b/web2.0.js
@@ -0,0 +1,106 @@
+/*
+    This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
+    https://thingshare.ion.nu/
+    Copyright (C) 2021  Alicia <...>
+
+    See /COPYING for license text (AGPLv3+)
+*/
+function postrequest(url, body, callback)
+{
+  if(fetch)
+  {
+    var options={'method':'POST', 'body':body, 'credentials':'same-origin', 'headers':{'Content-type':'application/x-www-form-urlencoded'}};
+    fetch(url, options).then(x=>x.text()).then(callback);
+  }else{
+    var req=new XMLHttpRequest();
+    req.open('POST',url,true);
+    req.onreadystatechange=function()
+    {
+      if(this.readyState==4){callback(this.responseText);}
+    };
+    req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+    req.send(body);
+  }
+}
+
+window.addEventListener('load', function()
+{
+  var commentform=document.getElementById('commentform');
+  if(commentform)
+  {
+    commentform.addEventListener('submit', function(evt)
+    {
+      var button=commentform.elements[commentform.elements.length-1];
+      button.textContent='Sending...'; // TODO: Localization?
+      button.disabled=true;
+      // Submit form
+      var nonce=encodeURIComponent(commentform.nonce.value);
+      var replyid=encodeURIComponent(commentform.replyto.value);
+      var msg=encodeURIComponent(commentform.msg.value);
+      postrequest('', 'nonce='+nonce+'&replyto='+replyid+'&msg='+msg+'&asyncrequest', function(response)
+      {
+        button.textContent='Send'; // TODO: Localization?
+        button.disabled=false;
+        // Handle response
+        if(response.substring(0,3)=='ok:')
+        {
+          // Insert message
+          var level=0;
+          var prev=false;
+          if(replyid>0) // Figure out the indentation level
+          {
+            prev=document.getElementsByName('comment'+replyid);
+            if(prev[0]){level=parseInt(prev[0].parentElement.parentElement.style.marginLeft)+15;}
+          }
+          var div=document.createElement('div');
+          div.className='message';
+          div.style.marginLeft=level+'px';
+          div.dataset.id=response.substring(3);
+          var div2=document.createElement('div');
+          div2.className='message_sender';
+          // User link
+          var profilelink=document.getElementById('profilelink');
+          var link=document.createElement('a');
+          link.name='comment'+response.substring(3);
+          link.href=profilelink.href;
+          link.textContent=profilelink.textContent;
+          div2.appendChild(link);
+          div2.appendChild(document.createTextNode(' '));
+          // Timestamp
+          var span=document.createElement('span');
+          span.className='time';
+          span.textContent='just now';
+          div2.appendChild(span);
+          div.appendChild(div2);
+          // Message
+          span=document.createElement('span');
+          span.innerHTML=Mdjs.md2html(commentform.msg.value.replace(/&/g,'&amp;amp;').replace(/</g,'&amp;lt;'));
+          div.appendChild(span);
+          span=document.createElement('p');
+          link=document.createElement('a');
+          link.href='#';
+          link.onclick=function(){replyto(this.parentElement.parentElement); return false;};
+          link.textContent='Reply';
+          span.appendChild(link);
+          div.appendChild(span);
+          var content=document.getElementById('content');
+          if(prev && prev[0] && prev[0].parentElement.parentElement.nextElementSibling)
+          {
+            content.insertBefore(div, prev[0].parentElement.parentElement.nextElementSibling);
+          }else{
+            content.insertBefore(div, commentform);
+          }
+          // Clear form
+          commentform.replyto.value='';
+          commentform.msg.value='';
+          document.getElementById('mdpreview').innerHTML='';
+          document.getElementById('replyto').value='';
+          document.getElementById('replyindicator').innerHTML='';
+        }else{ // TODO: display error better
+          alert('Error: '+response);
+        }
+      });
+      evt.preventDefault();
+    });
+  }
+});