Detailed changes
  
  
    
    @@ -37,4 +37,5 @@
     <string name="block_media">Block Media</string>
     <string name="new_contact">New Contact or Channel</string>
     <string name="pref_broadcast_last_activity_summary">Allow contacts to see when you were last active in the app</string>
+    <string name="visitor">Muted</string>
 </resources>
  
  
  
    
    @@ -1,5 +1,6 @@
 package eu.siacs.conversations.entities;
 
+import android.net.Uri;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -25,6 +26,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.forms.Field;
 import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xml.Element;
 
 public class MucOptions {
 
@@ -54,7 +56,7 @@ public class MucOptions {
         this.account = conversation.getAccount();
         this.conversation = conversation;
         final String nick = getProposedNick(conversation.getAttribute("mucNick"));
-        this.self = new User(this, createJoinJid(nick), nick);
+        this.self = new User(this, createJoinJid(nick), nick, new HashSet<>());
         this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
         this.self.role = Role.of(conversation.getAttribute("role"));
     }
@@ -338,7 +340,7 @@ public class MucOptions {
     public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
         User user = findUserByRealJid(jid);
         if (user == null) {
-            user = new User(this, fullJid, null);
+            user = new User(this, fullJid, null, new HashSet<>());
             user.setRealJid(jid);
         }
         return user;
@@ -537,7 +539,7 @@ public class MucOptions {
     private List<User> getFallbackUsersFromCryptoTargets() {
         List<User> users = new ArrayList<>();
         for (Jid jid : conversation.getAcceptedCryptoTargets()) {
-            User user = new User(this, null, null);
+            User user = new User(this, null, null, new HashSet<>());
             user.setRealJid(jid);
             users.add(user);
         }
@@ -781,6 +783,39 @@ public class MucOptions {
 
     }
 
+    public static class Hat implements Comparable<Hat> {
+        private final Uri uri;
+        private final String title;
+
+        public Hat(final Element el) {
+            Uri parseUri = null; // null hat uri is invaild per spec
+            try {
+                parseUri = Uri.parse(el.getAttribute("uri"));
+            } catch (final Exception e) { }
+            uri = parseUri;
+
+            title = el.getAttribute("title");
+        }
+
+        public Hat(final Uri uri, final String title) {
+            this.uri = uri;
+            this.title = title;
+        }
+
+        public String toString() {
+            return title;
+        }
+
+        public int getColor() {
+            return UIHelper.getColorForName(uri == null ? title : uri.toString());
+        }
+
+        @Override
+        public int compareTo(@NonNull Hat another) {
+            return title.compareTo(another.title);
+        }
+    }
+
     public static class User implements Comparable<User>, AvatarService.Avatarable {
         private Role role = Role.NONE;
         private Affiliation affiliation = Affiliation.NONE;
@@ -791,11 +826,13 @@ public class MucOptions {
         private Avatar avatar;
         private final MucOptions options;
         private ChatState chatState = Config.DEFAULT_CHAT_STATE;
+        private final Set<Hat> hats;
 
-        public User(MucOptions options, Jid fullJid, final String nick) {
+        public User(MucOptions options, Jid fullJid, final String nick, final Set<Hat> hats) {
             this.options = options;
             this.fullJid = fullJid;
             this.nick = nick == null ? getName() : nick;
+            this.hats = hats;
         }
 
         public String getName() {
@@ -822,6 +859,10 @@ public class MucOptions {
             this.affiliation = Affiliation.of(affiliation);
         }
 
+        public Set<Hat> getHats() {
+            return this.hats;
+        }
+
         public long getPgpKeyId() {
             if (this.pgpKeyId != 0) {
                 return this.pgpKeyId;
  
  
  
    
    @@ -7,6 +7,8 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
+import java.util.TreeSet;
 
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -132,10 +134,10 @@ public abstract class AbstractParser {
 	}
 
 	public static MucOptions.User parseItem(Conversation conference, Element item) {
-		return parseItem(conference,item,null,null);
+		return parseItem(conference,item,null,null,new Element("hats", "urn:xmpp:hats:0"));
 	}
 
-	public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final String nickname) {
+	public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final String nickname, final Element hatsEl) {
 		final String local = conference.getJid().getLocal();
 		final String domain = conference.getJid().getDomain().toEscapedString();
 		String affiliation = item.getAttribute("affiliation");
@@ -155,7 +157,13 @@ public abstract class AbstractParser {
 				nick = nickname;
 			}
 		} catch (final gnu.inet.encoding.PunycodeException | ArrayIndexOutOfBoundsException e) { }
-		MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid, nick);
+		Set<MucOptions.Hat> hats = new TreeSet<>();
+		for (Element hat : hatsEl.getChildren()) {
+			if ("hat".equals(hat.getName()) && ("urn:xmpp:hats:0".equals(hat.getNamespace()) || "xmpp:prosody.im/protocol/hats:1".equals(hat.getNamespace()))) {
+				hats.add(new MucOptions.Hat(hat));
+			}
+		}
+		MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid, nick, hats);
 		if (InvalidJid.isValid(realJid)) {
 			user.setRealJid(realJid);
 		}
  
  
  
    
    @@ -64,6 +64,11 @@ public class PresenceParser extends AbstractParser implements
 			final String type = packet.getAttribute("type");
 			final Element x = packet.findChild("x", Namespace.MUC_USER);
 			final Element nick = packet.findChild("nick", Namespace.NICK);
+			Element hats = packet.findChild("hats", "urn:xmpp:hats:0");
+			if (hats == null) {
+				hats = packet.findChild("hats", "xmpp:prosody.im/protocol/hats:1");
+			}
+			if (hats == null) hats = new Element("hats", "urn:xmpp:hats:0");
 			Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
 			final List<String> codes = getStatusCodes(x);
 			if (type == null) {
@@ -71,7 +76,7 @@ public class PresenceParser extends AbstractParser implements
 					Element item = x.findChild("item");
 					if (item != null && !from.isBareJid()) {
 						mucOptions.setError(MucOptions.Error.NONE);
-						MucOptions.User user = parseItem(conversation, item, from, nick == null ? null : nick.getContent());
+						MucOptions.User user = parseItem(conversation, item, from, nick == null ? null : nick.getContent(), hats);
 						if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) {
 							if (mucOptions.setOnline()) {
 								mXmppConnectionService.getAvatarService().clear(mucOptions);
@@ -175,7 +180,7 @@ public class PresenceParser extends AbstractParser implements
 				} else if (!from.isBareJid()){
 					Element item = x.findChild("item");
 					if (item != null) {
-						mucOptions.updateUser(parseItem(conversation, item, from, nick == null ? null : nick.getContent()));
+						mucOptions.updateUser(parseItem(conversation, item, from, nick == null ? null : nick.getContent(), hats));
 					}
 					MucOptions.User user = mucOptions.deleteUser(from);
 					if (user != null) {
  
  
  
    
    @@ -671,7 +671,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         return getStatus(this, user, mAdvancedMode);
     }
 
-
     @Override
     public void onAffiliationChangedSuccessful(Jid jid) {
         refreshUi();
  
  
  
    
    @@ -1,9 +1,11 @@
 package eu.siacs.conversations.ui.adapter;
 
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.IntentSender;
 import android.view.ContextMenu;
 import android.view.LayoutInflater;
+import android.widget.TextView;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -13,6 +15,9 @@ import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.ListAdapter;
 import androidx.recyclerview.widget.RecyclerView;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.openintents.openpgp.util.OpenPgpUtils;
 
 import eu.siacs.conversations.R;
@@ -82,17 +87,17 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
         });
         final String name = user.getNick();
         final Contact contact = user.getContact();
+        viewHolder.binding.contactJid.setVisibility(View.GONE);
+        viewHolder.binding.contactJid.setText("");
         if (contact != null) {
             final String displayName = contact.getDisplayName();
             viewHolder.binding.contactDisplayName.setText(displayName);
             if (name != null && !name.equals(displayName)) {
-                viewHolder.binding.contactJid.setText(String.format("%s \u2022 %s", name, ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)));
-            } else {
-                viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+                viewHolder.binding.contactJid.setVisibility(View.VISIBLE);
+                viewHolder.binding.contactJid.setText(name);
             }
         } else {
             viewHolder.binding.contactDisplayName.setText(name == null ? "" : name);
-            viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
         }
         if (advancedMode && user.getPgpKeyId() != 0) {
             viewHolder.binding.key.setVisibility(View.VISIBLE);
@@ -116,7 +121,36 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
             viewHolder.binding.key.setVisibility(View.GONE);
         }
 
+        viewHolder.binding.tags.setVisibility(View.VISIBLE);
+        viewHolder.binding.tags.removeAllViewsInLayout();
+        for (MucOptions.Hat hat : getPseudoHats(viewHolder.binding.getRoot().getContext(), user)) {
+            TextView tv = (TextView) LayoutInflater.from(viewHolder.binding.getRoot().getContext()).inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
+            tv.setText(hat.toString());
+            tv.setBackgroundColor(hat.getColor());
+            viewHolder.binding.tags.addView(tv);
+        }
+        for (MucOptions.Hat hat : user.getHats()) {
+            TextView tv = (TextView) LayoutInflater.from(viewHolder.binding.getRoot().getContext()).inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
+            tv.setText(hat.toString());
+            tv.setBackgroundColor(hat.getColor());
+            viewHolder.binding.tags.addView(tv);
+        }
 
+        if (viewHolder.binding.tags.getChildCount() < 1) {
+            viewHolder.binding.contactJid.setVisibility(View.VISIBLE);
+            viewHolder.binding.tags.setVisibility(View.GONE);
+        }
+    }
+
+    private List<MucOptions.Hat> getPseudoHats(Context context, MucOptions.User user) {
+        List<MucOptions.Hat> hats = new ArrayList<>();
+        if (user.getAffiliation() != MucOptions.Affiliation.NONE) {
+            hats.add(new MucOptions.Hat(null, context.getString(user.getAffiliation().getResId())));
+        }
+        if (user.getRole() != MucOptions.Role.PARTICIPANT) {
+            hats.add(new MucOptions.Hat(null, context.getString(user.getRole().getResId())));
+        }
+        return hats;
     }
 
     public MucOptions.User getSelectedUser() {