more muc options

Daniel Gultsch created

* show invite button only with admin privileges or on public conferences
* Offer to ban user in public conferences.
Thanks to @betheg for the awesome ground work for this

Change summary

src/main/java/eu/siacs/conversations/entities/MucOptions.java            | 30 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           | 23 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 30 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   | 27 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java        | 11 
src/main/res/values/strings.xml                                          |  3 
6 files changed, 99 insertions(+), 25 deletions(-)

Detailed changes

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

@@ -4,6 +4,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.xml.Element;
@@ -12,6 +13,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 
 import android.annotation.SuppressLint;
+import android.util.Log;
 
 @SuppressLint("DefaultLocale")
 public class MucOptions {
@@ -51,8 +53,6 @@ public class MucOptions {
 		}
 	}
 
-	;
-
 	public enum Role {
 		MODERATOR("moderator", R.string.moderator),
 		VISITOR("visitor", R.string.visitor),
@@ -86,6 +86,7 @@ public class MucOptions {
 
 	public static final int KICKED_FROM_ROOM = 9;
 
+	public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
 	public static final String STATUS_CODE_SELF_PRESENCE = "110";
 	public static final String STATUS_CODE_BANNED = "301";
 	public static final String STATUS_CODE_CHANGED_NICK = "303";
@@ -107,8 +108,8 @@ public class MucOptions {
 	}
 
 	public class User {
-		private Role role;
-		private Affiliation affiliation;
+		private Role role = Role.NONE;
+		private Affiliation affiliation = Affiliation.NONE;
 		private String name;
 		private Jid jid;
 		private long pgpKeyId = 0;
@@ -190,6 +191,7 @@ public class MucOptions {
 
 	private Account account;
 	private List<User> users = new CopyOnWriteArrayList<>();
+	private List<String> features = new ArrayList<>();
 	private Conversation conversation;
 	private boolean isOnline = false;
 	private int error = ERROR_UNKNOWN;
@@ -205,6 +207,23 @@ public class MucOptions {
 		this.conversation = conversation;
 	}
 
+	public void updateFeatures(ArrayList<String> features) {
+		this.features.clear();
+		this.features.addAll(features);
+	}
+
+	public boolean hasFeature(String feature) {
+		return this.features.contains(feature);
+	}
+
+	public boolean canInvite() {
+		 return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN);
+	}
+
+	public boolean membersOnly() {
+		return hasFeature("muc_membersonly");
+	}
+
 	public void deleteUser(String name) {
 		for (int i = 0; i < users.size(); ++i) {
 			if (users.get(i).getName().equals(name)) {
@@ -225,6 +244,7 @@ public class MucOptions {
 	}
 
 	public void processPacket(PresencePacket packet, PgpEngine pgp) {
+		Log.d(Config.LOGTAG, packet.toString());
 		final Jid from = packet.getFrom();
 		if (!from.isBareJid()) {
 			final String name = from.getResourcepart();
@@ -320,7 +340,7 @@ public class MucOptions {
 	}
 
 	private List<String> getStatusCodes(Element x) {
-		List<String> codes = new ArrayList<String>();
+		List<String> codes = new ArrayList<>();
 		if (x != null) {
 			for (Element child : x.getChildren()) {
 				if (child.getName().equals("status")) {

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

@@ -1,15 +1,13 @@
 package eu.siacs.conversations.parser;
 
-import android.util.Log;
-
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 
-import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
@@ -142,14 +140,25 @@ public class MessageParser extends AbstractParser implements
 		Conversation conversation = mXmppConnectionService
 				.findOrCreateConversation(account, from.toBareJid(), true);
 		if (packet.hasChild("subject")) {
-			conversation.getMucOptions().setSubject(
-					packet.findChild("subject").getContent());
+			conversation.getMucOptions().setSubject(packet.findChild("subject").getContent());
 			mXmppConnectionService.updateConversationUi();
 			return null;
 		}
-		if (from.isBareJid()) {
+
+		final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
+		if (from.isBareJid() && (x == null || !x.hasChild("status"))) {
 			return null;
+		} else if (from.isBareJid() && x.hasChild("status")) {
+			for(Element child : x.getChildren()) {
+				if (child.getName().equals("status")) {
+					String code = child.getAttribute("code");
+					if (code.contains(MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED)) {
+						mXmppConnectionService.fetchConferenceConfiguration(conversation);
+					}
+				}
+			}
 		}
+
 		if (from.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
 			if (mXmppConnectionService.markMessage(conversation,
 					packet.getId(), Message.STATUS_SEND)) {
@@ -350,7 +359,7 @@ public class MessageParser extends AbstractParser implements
 		final Jid from = packet.getAttributeAsJid("from");
 		Element invite = extractInvite(packet);
 		if (invite != null) {
-			Conversation conversation = mXmppConnectionService.findOrCreateConversation(account,from, true);
+			Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true);
 			if (!conversation.getMucOptions().online()) {
 				Element password = invite.findChild("password");
 				conversation.getMucOptions().setPassword(password == null ? null : password.getContent());

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1316,6 +1316,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 				packet.addChild("x", "jabber:x:signed").setContent(sig);
 			}
 			sendPresencePacket(account, packet);
+			fetchConferenceConfiguration(conversation);
 			if (!joinJid.equals(conversation.getJid())) {
 				conversation.setContactJid(joinJid);
 				databaseBackend.updateConversation(conversation);
@@ -1475,6 +1476,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		}
 	}
 
+	public void fetchConferenceConfiguration(final Conversation conversation) {
+		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+		request.setTo(conversation.getJid().toBareJid());
+		request.query("http://jabber.org/protocol/disco#info");
+		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				if (packet.getType() != IqPacket.TYPE.ERROR) {
+					ArrayList<String> features = new ArrayList<String>();
+					for (Element child : packet.query().getChildren()) {
+						if (child != null && child.getName().equals("feature")) {
+							String var = child.getAttribute("var");
+							if (var != null) {
+								features.add(var);
+							}
+						}
+					}
+					conversation.getMucOptions().updateFeatures(features);
+				}
+			}
+		});
+	}
+
 	public void pushConferenceConfiguration(final Conversation conversation,final Bundle options, final OnConferenceOptionsPushed callback) {
 		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
 		request.setTo(conversation.getJid().toBareJid());
@@ -1520,14 +1544,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		final Jid jid = user.toBareJid();
 		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
 		Log.d(Config.LOGTAG,request.toString());
-		sendIqPacket(conference.getAccount(),request,new OnIqPacketReceived() {
+		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
 			@Override
 			public void onIqPacketReceived(Account account, IqPacket packet) {
-				Log.d(Config.LOGTAG,packet.toString());
+				Log.d(Config.LOGTAG, packet.toString());
 				if (packet.getType() == IqPacket.TYPE.RESULT) {
 					callback.onAffiliationChangedSuccessful(jid);
 				} else {
-					callback.onAffiliationChangeFailed(jid,R.string.could_not_change_affiliation);
+					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
 				}
 			}
 		});

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java 🔗

@@ -1,8 +1,10 @@
 package eu.siacs.conversations.ui;
 
 import android.annotation.TargetApi;
+import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.IntentSender.SendIntentException;
 import android.graphics.Bitmap;
 import android.os.Build;
@@ -285,13 +287,31 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 				xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
 				return true;
 			case R.id.remove_from_room:
-				xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
+				removeFromRoom(mSelectedUser);
 				return true;
 			default:
 				return super.onContextItemSelected(item);
 		}
 	}
 
+	private void removeFromRoom(final User user) {
+		if (mConversation.getMucOptions().membersOnly()) {
+			xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this);
+		} else {
+			AlertDialog.Builder builder = new AlertDialog.Builder(this);
+			builder.setTitle(R.string.ban_user_from_conference);
+			builder.setMessage(getString(R.string.removing_from_public_conference,user.getName()));
+			builder.setNegativeButton(R.string.cancel,null);
+			builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() {
+				@Override
+				public void onClick(DialogInterface dialog, int which) {
+					xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
+				}
+			});
+			builder.create().show();
+		}
+	}
+
 	protected void startConversation(User user) {
 		if (user.getJid() != null) {
 			Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false);
@@ -397,6 +417,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 			ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
 			iv.setImageBitmap(bm);
 			membersView.addView(view);
+			if (mConversation.getMucOptions().canInvite()) {
+				mInviteButton.setVisibility(View.VISIBLE);
+			} else {
+				mInviteButton.setVisibility(View.GONE);
+			}
 		}
 	}
 

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -9,7 +9,6 @@ import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
-import android.media.MediaActionSound;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -83,11 +82,6 @@ public class ConversationActivity extends XmppActivity
 
 	private boolean mActivityPaused = false;
 
-
-	public List<Conversation> getConversationList() {
-		return this.conversationList;
-	}
-
 	public Conversation getSelectedConversation() {
 		return this.mSelectedConversation;
 	}
@@ -284,8 +278,7 @@ public class ConversationActivity extends XmppActivity
 		final MenuItem menuBlock = menu.findItem(R.id.action_block);
 		final MenuItem menuUnblock = menu.findItem(R.id.action_unblock);
 
-		if (isConversationsOverviewVisable()
-				&& isConversationsOverviewHideable()) {
+		if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
 			menuArchive.setVisible(false);
 			menuMucDetails.setVisible(false);
 			menuContactDetails.setVisible(false);
@@ -309,9 +302,9 @@ public class ConversationActivity extends XmppActivity
 					menuAttach.setVisible(false);
 					menuBlock.setVisible(false);
 					menuUnblock.setVisible(false);
+					menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
 				} else {
 					menuMucDetails.setVisible(false);
-					menuInviteContact.setTitle(R.string.conference_with);
 					if (this.getSelectedConversation().isBlocked()) {
 						menuBlock.setVisible(false);
 					} else {

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

@@ -417,4 +417,7 @@
 	<string name="remove_admin_privileges">Remove admin privileges</string>
 	<string name="remove_from_room">Remove from room</string>
 	<string name="could_not_change_affiliation">Could not change affiliation</string>
+	<string name="ban_user_from_conference">Ban user from conference</string>
+	<string name="removing_from_public_conference">You are trying to remove %s from a public conference. The only way to do that is to ban that user for ever.</string>
+	<string name="ban_now">Ban now</string>
 </resources>