Affiliation and role and hats as tags

Stephen Paul Weber created

Do not show "none" affiliation since it doesn't really mean much and is
the default, same for "participant".  Show "visitor" role as "muted".

Hats are coloured by the URI not their name.

Change summary

src/cheogram/res/values/strings.xml                                    |  1 
src/main/java/eu/siacs/conversations/entities/MucOptions.java          | 49 
src/main/java/eu/siacs/conversations/parser/AbstractParser.java        | 14 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java        |  9 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java |  1 
src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java       | 42 
6 files changed, 102 insertions(+), 14 deletions(-)

Detailed changes

src/cheogram/res/values/strings.xml 🔗

@@ -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>

src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -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;

src/main/java/eu/siacs/conversations/parser/AbstractParser.java 🔗

@@ -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);
 		}

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -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) {

src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java 🔗

@@ -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() {