$ git clone https://ion.nu/git/webchat
commit 00eac05cabfee688e50623af4c6f913629ffad1a
Author: Alicia <...>
Date:   Fri Jan 1 00:09:18 2016 +0100

    Added moderator indicators in the userlist, a green dot next to the nickname with a descriptive title/hovertext

diff --git a/chat.css b/chat.css
index 4391be2..0ceeed3 100644
--- a/chat.css
+++ b/chat.css
@@ -287,3 +287,13 @@ button#deregisterchannel_button
 {
   display:none; /* Start hidden */
 }
+span.modindicator
+{
+  cursor:default;
+  color:#00ff00;
+  text-shadow:0px 0px 1px #000000;
+  -moz-text-shadow:0px 0px 1px #000000;
+  -webkit-text-shadow:0px 0px 1px #000000;
+  -ms-text-shadow:0px 0px 1px #000000;
+  -o-text-shadow:0px 0px 1px #000000;
+}
diff --git a/chat.js b/chat.js
index 1346ec9..14840dd 100644
--- a/chat.js
+++ b/chat.js
@@ -24,12 +24,35 @@
 var userlist=new Array();
 var nickname=''; // Will be set with 'startnick:'
 var me;
+function user_setmod(modprivileges)
+{
+  this.modprivileges=modprivileges;
+  if(modprivileges>0)
+  {
+    chatnote(this.nickname+' is a moderator');
+    if(!this.modindicator)
+    {
+      this.modindicator=document.createElement('span');
+      this.modindicator.innerHTML='&#x25cf;';
+      this.modindicator.className='modindicator';
+      this.modindicator.title='This user is a moderator of this channel';
+      this.listbox.appendChild(this.modindicator);
+    }
+  }else{
+    chatnote(this.nickname+' is no longer a moderator');
+    if(this.modindicator)
+    {
+      this.listbox.removeChild(this.modindicator);
+      this.modindicator=false;
+    }
+  }
+}
 function User(name)
 {
   this.nickname=name;
-  this.listlabel=document.createElement("span");
+  this.listbox=document.createElement('div');
+  this.listlabel=document.createElement('span');
   this.listlabel.textContent=this.nickname;
-  this.listlabel.style.display='block';
   var user=this;
   this.listlabel.oncontextmenu=function(e)
 //  this.listlabel.onclick=function(e)
@@ -37,10 +60,12 @@ function User(name)
     usermenu(user, e.pageX, e.pageY);
     return false;
   };
+  this.listbox.appendChild(this.listlabel);
   this.color='000000';
   this.modprivileges=0;
   this.broadcasting=false;
-  document.getElementById('userlist').appendChild(this.listlabel);
+  this.setmod=user_setmod;
+  document.getElementById('userlist').appendChild(this.listbox);
 }
 function finduser(name)
 {
diff --git a/proto.js b/proto.js
index ddc5f6a..d118463 100644
--- a/proto.js
+++ b/proto.js
@@ -112,7 +112,7 @@ function handlecommands(data)
     if(tab=findtab(user)){tab.dead=true; chatnoteon(user+' quit', tab);}
     if(user=finduser(user))
     {
-      document.getElementById('userlist').removeChild(user.listlabel);
+      document.getElementById('userlist').removeChild(user.listbox);
       userlist.splice(userlist.indexOf(user), 1);
       closecam(user);
     }
@@ -191,13 +191,7 @@ function handlecommands(data)
     var sep=data.data.indexOf(':', 4);
     if(sep<0){alert("Error, no separator found");}
     var user=finduser(data.data.substring(4,sep));
-    user.modprivileges=parseInt(data.data.substring(sep+1, data.data.length));
-    if(user.modprivileges)
-    {
-      chatnote(user.nickname+' is a moderator');
-    }else{
-      chatnote(user.nickname+' is no longer a moderator');
-    }
+    user.setmod(parseInt(data.data.substring(sep+1, data.data.length)));
     if(user==me)
     {
       // Show applicable channel operations for the given privileges