$ git clone http://thingshare.ion.nu/thingshare.git
commit 3ef20aa79f714eb209a0149111517edc6f7660f0
Author: Alicia <...>
Date:   Sat Jan 30 01:09:55 2021 +0100

    Added support for notifications.

diff --git a/Licenses b/Licenses
index e58aa7d..830c5d5 100644
--- a/Licenses
+++ b/Licenses
@@ -4,4 +4,5 @@ External resources used by Thingshare, and their respective licenses:
 parsedown: under the terms of the MIT license (https://github.com/erusev/parsedown)
 mdjs: under the terms of the Apache license (https://github.com/hangxingliu/mdjs)
 icons/default.svg: "Editor, software, text icon" from the Macaron icon set by Goescat Wei, under the terms of the Creative Commons Attribution 3.0 Unported (https://www.iconfinder.com/icons/3246744/editor_software_text_icon)
+icons/bell.svg/png: "Alarm, alert, attention, bell, notification, notifications, ring icon" by DIVYA A under the terms of the Creative Commons Attribution 3.0 Unported (https://www.iconfinder.com/icons/4964018/alarm_alert_attention_bell_notification_notifications_ring_icon)
 icons/scad.png: GNU GPLv2 (https://github.com/openscad/openscad/blob/master/icons/SCAD.png)
diff --git a/db.php b/db.php
index 345f8b2..ffdd2b4 100644
--- a/db.php
+++ b/db.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
@@ -130,6 +130,12 @@ function db_create_tables()
     sent datetime,
     removed boolean);');
   mysqli_query($db, 'create table filtertags(tag text);');
+  mysqli_query($db, 'create table notifications(id integer primary key auto_increment,
+    user integer,
+    message text,
+    link text,
+    sent datetime,
+    seen boolean);');
 }
 
 function db_getuser($id)
diff --git a/docs/RPCs b/docs/RPCs
index 35fde7b..c5fc97f 100644
--- a/docs/RPCs
+++ b/docs/RPCs
@@ -142,3 +142,11 @@ Signed request format:
 }
 Return format:
 {"status":"OK"} or {"error":<Error message>}
+
+RPC: notification/<Username>
+Send a notification (about a comment, maybe the only thing notifications will be needed for)
+Signed request format:
+{
+  "message": <Message>,
+  "link": <...>
+}
diff --git a/head.php b/head.php
index 4784a3f..23e63cb 100644
--- a/head.php
+++ b/head.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
@@ -20,6 +20,34 @@
 include_once('config.php');
 $menu=''; // Additional menu items depending on privileges/just being logged in
 $unreadmsgs='';
+$notifications='';
+$notifications_new=false;
+function timeago($time)
+{
+  $time=time()-strtotime($time);
+  $unit='';
+  if($time>3600*47)
+  {
+    $time=round($time/(3600*24));
+    $unit='day';
+  }
+  elseif($time>3600)
+  {
+    $time=round($time/3600);
+    $unit='hour';
+  }
+  elseif($time>60)
+  {
+    $time=round($time/60);
+    $unit='minute';
+  }
+  if($unit!='')
+  {
+    // TODO: Use gettext pluralization
+    return $time.' '.$unit.($time==1?'':'s').' ago';
+  }
+  return 'just now';
+}
 if(isset($_COOKIE['PHPSESSID'])) // TODO: See if there's a better way to check if there is a session to resume
 {
   session_start();
@@ -39,6 +67,28 @@ if(isset($_COOKIE['PHPSESSID'])) // TODO: See if there's a better way to check i
     // Check for unread messages
     $res=mysqli_query($db, 'select id from messages where user='.(int)$_SESSION['id'].' and !msgread limit 1');
     if(mysqli_num_rows($res)>0){$unreadmsgs=' class="highlight"';}
+    // Check for notifications
+    if(isset($_GET['notification']))
+    {
+      // TODO: Is there a cleaner way to update 'seen' than passing the ID in GET?
+      mysqli_query($db, 'update notifications set seen=true where user='.(int)$_SESSION['id'].' and id='.(int)$_GET['notification']);
+    }
+    $res=mysqli_query($db, 'select id, message, link, sent, seen from notifications where user='.(int)$_SESSION['id'].' order by sent desc limit 20');
+    while($row=mysqli_fetch_assoc($res))
+    {
+      $link=$row['link'];
+      if(!$row['seen'])
+      {
+        $notifications_new=true;
+        if($pos=strpos($link, '#'))
+        {
+          $link=substr($link,0,$pos).'?notification='.$row['id'].substr($link,$pos);
+        }else{
+          $link.='?notification='.$row['id'];
+        }
+      }
+      $notifications.=timeago($row['sent']).' <a href="'.BASEURL.$link.'"><div class="notification'.($row['seen']?'':' notification-unseen').'">'.$row['message'].'</div></a><br />';
+    }
   }
 }
 $loginlink=BASEURL.'/login?returnto='.urlencode($_SERVER['REQUEST_URI']);
@@ -69,6 +119,12 @@ if($path[1]=='tag')
       <?=$menu?>
     </div>
     <div id="user"><?php if(isset($_SESSION['name'])){ ?>
+      <div class="dropdown-container">
+        <div class="dropdown">
+          <img src="<?=BASEURL?>/icons/bell.svg" width="32" height="32" onerror="this.src='<?=BASEURL?>/icons/bell.png';" style="display:block; opacity:<?=($notifications_new?1:0.5)?>;" />
+          <?=$notifications?>
+        </div>
+      </div>
       <!-- Link to user settings, messages -->
       <a href="<?=BASEURL?>/user/<...>">My profile</a>
       <a href="<?=BASEURL?>/messages"<?=$unreadmsgs?>>Messages</a>
diff --git a/icons/bell.png b/icons/bell.png
new file mode 100644
index 0000000..85c1346
Binary files /dev/null and b/icons/bell.png differ
diff --git a/icons/bell.svg b/icons/bell.svg
new file mode 100644
index 0000000..363c2e5
--- /dev/null
+++ b/icons/bell.svg
@@ -0,0 +1 @@
+<?xml version="1.0" ?><svg height="24" version="1.1" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><title/><path d="M18 15.984l2.016 2.016v0.984h-16.031v-0.984l2.016-2.016v-4.969c0-3.094 1.641-5.625 4.5-6.328v-0.703c0-0.844 0.656-1.5 1.5-1.5s1.5 0.656 1.5 1.5v0.703c2.859 0.703 4.5 3.281 4.5 6.328v4.969zM12 21.984c-1.125 0-2.016-0.891-2.016-1.969h4.031c0 1.078-0.938 1.969-2.016 1.969z"/></svg>
\ No newline at end of file
diff --git a/index.php b/index.php
index 082f6a1..e6e1c4f 100644
--- a/index.php
+++ b/index.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
@@ -72,6 +72,7 @@ switch($path[1])
       case 'messages': include('rpc_messages.php'); exit();
       case 'peers': include('rpc_peers.php'); exit();
       case 'comments': include('rpc_comments.php'); exit();
+      case 'notification': include('rpc_notification.php'); exit();
     }
     header('HTTP/1.1 404 Not found');
     die('{"httpresponse":404,"error":"Not found"}');
diff --git a/rpc_comments.php b/rpc_comments.php
index 37ac376..283d8ec 100644
--- a/rpc_comments.php
+++ b/rpc_comments.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
@@ -54,6 +54,28 @@ if(isset($_POST['data'])) // Posting a comment
   if(mysqli_query($db, $q))
   {
     print('{"status":"OK"}');
+    // Notify the parent commenter or thing's owner
+    $link='/thing/'.$thing.'@'.DOMAIN.'#comment'.mysqli_insert_id($db);
+    $name=getdisplayname($obj['from'].'@'.$peer);
+    $res=mysqli_query($db, 'select name, user from things where thingid='.$thing.' order by posted desc limit 1');
+    $thingrow=mysqli_fetch_assoc($res);
+    $thingname=$thingrow['name'];
+    if($replyto)
+    {
+      $res=mysqli_query($db, 'select sender from comments where id='.$replyto);
+      $row=mysqli_fetch_row($res);
+      $touser=explode('@', $row[0]);
+      if(count($touser)==2)
+      {
+        $msg=Array('message'=>$name.' replied to your comment on '.$thingname,
+                   'link'=>$link);
+        rpc_post($touser[1], 'notification/'.$touser[0], $msg);
+      }
+    }else{
+      $msg=Array('message'=>$name.' commented on '.$thingname,
+                 'link'=>$link);
+      rpc_post(DOMAIN, 'notification/'.db_getuser($thingrow['user']), $msg);
+    }
   }else{
     print('{"error":"Database error"}');
   }
diff --git a/rpc_notification.php b/rpc_notification.php
new file mode 100644
index 0000000..0f47efc
--- /dev/null
+++ b/rpc_notification.php
@@ -0,0 +1,43 @@
+<...>
+
+    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
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    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('rpc.php');
+include_once('db.php');
+$obj=rpc_verifypost($peer);
+// Find recipient user ID
+$to=mysqli_real_escape_string($db, $path[3]);
+$res=mysqli_query($db, 'select id from users where name="'.$to.'"');
+$res=mysqli_fetch_row($res);
+if(!$res){header('HTTP/1.1 404 Not found'); die('{"error":"User not found"}');}
+// TODO: Check that the link refers to something on the sender node?
+// Prepare values for DB
+$user=(int)$res[0];
+$msg=mysqli_real_escape_string($db, $obj['message']);
+$link=mysqli_real_escape_string($db, $obj['link']);
+$timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s'));
+// Store notification
+$q='insert into notifications(user, message, link, sent, seen) ';
+$q.='values('.$user.', "'.$msg.'", "'.$link.'", "'.$timestamp.'", false)';
+if(mysqli_query($db, $q))
+{
+  print('{"status":"OK"}');
+}else{
+  print('{"error":"Database error"}');
+}
+?>
diff --git a/style.css b/style.css
index 882e9fe..f316e7e 100644
--- a/style.css
+++ b/style.css
@@ -1,7 +1,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
@@ -171,3 +171,31 @@ a.highlight {
 a.pagenav {
   margin-right:12px;
 }
+div.dropdown-container {
+  display:inline-block;
+  vertical-align:top;
+  margin-top:-8px;
+  width:32px;
+  height:32px;
+}
+div.dropdown {
+  width:32px;
+  height:32px;
+  overflow:hidden;
+  position:absolute;
+}
+div.dropdown:hover {
+  overflow-y:auto;
+  height:250px;
+  width:auto;
+  background-color:#e0e0e0;
+  border-radius:5px;
+  box-shadow:#000000 0px 15px 20px 0px;
+  z-index:20;
+}
+div.notification {
+  display:inline;
+}
+div.notification-unseen {
+  font-weight:bold;
+}