MucDetailsContextMenuHelper.java

  1package eu.siacs.conversations.ui.util;
  2
  3import android.app.Activity;
  4import android.preference.PreferenceManager;
  5import android.text.SpannableString;
  6import android.text.Spanned;
  7import android.text.style.TypefaceSpan;
  8import android.util.Pair;
  9import android.view.ContextMenu;
 10import android.view.Menu;
 11import android.view.MenuItem;
 12import android.view.View;
 13import android.widget.Toast;
 14
 15import androidx.appcompat.app.AlertDialog;
 16import androidx.databinding.DataBindingUtil;
 17
 18import java.util.ArrayList;
 19
 20import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 21import eu.siacs.conversations.Config;
 22import eu.siacs.conversations.R;
 23import eu.siacs.conversations.databinding.DialogQuickeditBinding;
 24import eu.siacs.conversations.entities.Account;
 25import eu.siacs.conversations.entities.Contact;
 26import eu.siacs.conversations.entities.Conversation;
 27import eu.siacs.conversations.entities.Message;
 28import eu.siacs.conversations.entities.MucOptions;
 29import eu.siacs.conversations.entities.MucOptions.User;
 30import eu.siacs.conversations.services.XmppConnectionService;
 31import eu.siacs.conversations.ui.ConferenceDetailsActivity;
 32import eu.siacs.conversations.ui.ConversationFragment;
 33import eu.siacs.conversations.ui.ConversationsActivity;
 34import eu.siacs.conversations.ui.MucUsersActivity;
 35import eu.siacs.conversations.ui.XmppActivity;
 36import eu.siacs.conversations.utils.UIHelper;
 37import eu.siacs.conversations.xmpp.Jid;
 38import eu.siacs.conversations.xml.Element;
 39
 40public final class MucDetailsContextMenuHelper {
 41    private static final int ACTION_BAN = 0;
 42    private static final int ACTION_GRANT_MEMBERSHIP = 1;
 43    private static final int ACTION_REMOVE_MEMBERSHIP = 2;
 44    private static final int ACTION_GRANT_ADMIN = 3;
 45    private static final int ACTION_REMOVE_ADMIN = 4;
 46    private static final int ACTION_GRANT_OWNER = 5;
 47    private static final int ACTION_REMOVE_OWNER = 6;
 48
 49    public static void onCreateContextMenu(ContextMenu menu, View v) {
 50        final XmppActivity activity = XmppActivity.find(v);
 51        final Object tag = v.getTag();
 52        if (tag instanceof User user && activity != null) {
 53            activity.getMenuInflater().inflate(R.menu.muc_details_context, menu);
 54            String name;
 55            final Contact contact = user.getContact();
 56            if (contact != null && contact.showInContactList()) {
 57                name = contact.getDisplayName();
 58            } else if (user.getRealJid() != null) {
 59                name = user.getRealJid().asBareJid().toString();
 60            } else {
 61                name = user.getNick();
 62            }
 63            menu.setHeaderTitle(name);
 64            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(
 65                    activity, menu, user.getConversation(), user);
 66        }
 67    }
 68
 69    public static Pair<CharSequence[], Integer[]> getPermissionsChoices(Activity activity, Conversation conversation, User user) {
 70        ArrayList<CharSequence> items = new ArrayList<>();
 71        ArrayList<Integer> actions = new ArrayList<>();
 72        final User self = conversation.getMucOptions().getSelf();
 73        final MucOptions mucOptions = conversation.getMucOptions();
 74        final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous();
 75        if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) || self.getAffiliation() == MucOptions.Affiliation.OWNER) {
 76            if (!Config.DISABLE_BAN && user.getAffiliation() != MucOptions.Affiliation.OUTCAST) {
 77                items.add(activity.getString(isGroupChat ? R.string.ban_from_conference : R.string.ban_from_channel));
 78                actions.add(ACTION_BAN);
 79            } else if (!Config.DISABLE_BAN) {
 80                items.add(isGroupChat ? "Unban from group chat" : "Unban from channel");
 81                actions.add(ACTION_REMOVE_MEMBERSHIP);
 82            }
 83            if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
 84                items.add(activity.getString(R.string.grant_membership));
 85                actions.add(ACTION_GRANT_MEMBERSHIP);
 86            } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) {
 87                items.add(activity.getString(R.string.remove_membership));
 88                actions.add(ACTION_REMOVE_MEMBERSHIP);
 89            }
 90        }
 91        if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
 92            if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) {
 93                items.add(activity.getString(R.string.grant_admin_privileges));
 94                actions.add(ACTION_GRANT_ADMIN);
 95            } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
 96                items.add(activity.getString(R.string.remove_admin_privileges));
 97                actions.add(ACTION_REMOVE_ADMIN);
 98            }
 99            if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
100                items.add(activity.getString(R.string.grant_owner_privileges));
101                actions.add(ACTION_GRANT_OWNER);
102            } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER){
103                items.add(activity.getString(R.string.remove_owner_privileges));
104                actions.add(ACTION_REMOVE_OWNER);
105            }
106        }
107        return new Pair<>(items.toArray(new CharSequence[items.size()]), actions.toArray(new Integer[actions.size()]));
108    }
109
110    public static void configureMucDetailsContextMenu(
111            XmppActivity activity, Menu menu, Conversation conversation, User user) {
112        final MucOptions mucOptions = conversation.getMucOptions();
113        final boolean advancedMode =
114                PreferenceManager.getDefaultSharedPreferences(activity)
115                        .getBoolean("advanced_muc_mode", false);
116        final boolean showMucPm = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("show_muc_pm", false);
117        final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous();
118        MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
119        MenuItem shareContactDetails = menu.findItem(R.id.share_contact_details);
120
121        MenuItem blockAvatar = menu.findItem(R.id.action_block_avatar);
122        if (user != null && user.getAvatar() != null) {
123            blockAvatar.setVisible(true);
124        }
125
126        MenuItem muteParticipant = menu.findItem(R.id.action_mute_participant);
127        MenuItem unmuteParticipant = menu.findItem(R.id.action_unmute_participant);
128        if (user != null && user.getOccupantId() != null) {
129            if (activity.xmppConnectionService.isMucUserMuted(user)) {
130                unmuteParticipant.setVisible(true);
131            } else {
132                muteParticipant.setVisible(true);
133            }
134        }
135
136        if (user != null && user.getRealJid() != null) {
137            MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
138            MenuItem startConversation = menu.findItem(R.id.start_conversation);
139            MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
140            MenuItem managePermissions = menu.findItem(R.id.manage_permissions);
141            removeFromRoom.setTitle(
142                    isGroupChat ? R.string.remove_from_room : R.string.remove_from_channel);
143            MenuItem invite = menu.findItem(R.id.invite);
144            startConversation.setVisible(true);
145            final Contact contact = user.getContact();
146            final User self = conversation.getMucOptions().getSelf();
147            if ((contact != null && contact.showInRoster())
148                    || mucOptions.isPrivateAndNonAnonymous()) {
149                showContactDetails.setVisible(contact == null || !contact.isSelf());
150            }
151            if ((activity instanceof ConferenceDetailsActivity
152                            || activity instanceof MucUsersActivity)
153                    && user.getRole() == MucOptions.Role.NONE) {
154                invite.setVisible(true);
155            }
156            boolean managePermissionsVisible = false;
157            if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) || self.getAffiliation() == MucOptions.Affiliation.OWNER) {
158                if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
159                    managePermissionsVisible = true;
160                } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) {
161                    managePermissionsVisible = true;
162                }
163                if (!Config.DISABLE_BAN) {
164                    managePermissionsVisible = true;
165                }
166            }
167            if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
168                if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
169                    managePermissionsVisible = true;
170                } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER){
171                    managePermissionsVisible = true;
172                }
173                if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) {
174                    managePermissionsVisible = true;
175                } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
176                    managePermissionsVisible = true;
177                }
178            }
179            managePermissions.setVisible(managePermissionsVisible);
180            sendPrivateMessage.setVisible(showMucPm && user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
181            shareContactDetails.setVisible(user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
182        } else {
183            sendPrivateMessage.setVisible(showMucPm && user != null && user.isOnline());
184            sendPrivateMessage.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
185            shareContactDetails.setVisible(user != null && user.isOnline());
186            shareContactDetails.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
187        }
188    }
189
190    public static boolean onContextItemSelected(MenuItem item, User user, XmppActivity activity) {
191        return onContextItemSelected(item, user, activity, null);
192    }
193
194    public static void maybeModerateRecent(XmppActivity activity, Conversation conversation, User user) {
195        if (!conversation.getMucOptions().getSelf().getRole().ranks(MucOptions.Role.MODERATOR) || !conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) return;
196
197        DialogQuickeditBinding binding = DataBindingUtil.inflate(activity.getLayoutInflater(), R.layout.dialog_quickedit, null, false);
198        binding.inputEditText.setText("Spam");
199        new AlertDialog.Builder(activity)
200            .setTitle(R.string.moderate_recent)
201            .setMessage("Do you want to moderate all recent messages from this user?")
202            .setView(binding.getRoot())
203            .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
204                for (Message m : conversation.findMessagesBy(user)) {
205                    activity.xmppConnectionService.moderateMessage(conversation.getAccount(), m, binding.inputEditText.getText().toString());
206                }
207            })
208            .setNegativeButton(R.string.no, null).show();
209    }
210
211    public static boolean onContextItemSelected(
212            MenuItem item, User user, XmppActivity activity, final String fingerprint) {
213        final Conversation conversation = user.getConversation();
214        final XmppConnectionService.OnAffiliationChanged onAffiliationChanged =
215                activity instanceof XmppConnectionService.OnAffiliationChanged
216                        ? (XmppConnectionService.OnAffiliationChanged) activity
217                        : null;
218        Jid jid = user.getRealJid();
219        switch (item.getItemId()) {
220            case R.id.action_contact_details:
221                final Jid realJid = user.getRealJid();
222                final Account account = conversation.getAccount();
223                final Contact contact =
224                        realJid == null ? null : account.getRoster().getContact(realJid);
225                if (contact != null) {
226                    activity.switchToContactDetails(contact, fingerprint);
227                }
228                return true;
229            case R.id.action_block_avatar:
230                new AlertDialog.Builder(activity)
231                    .setTitle(R.string.block_media)
232                    .setMessage("Do you really want to block this avatar?")
233                    .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
234                        activity.xmppConnectionService.blockMedia(
235                            activity.xmppConnectionService.getFileBackend().getAvatarFile(user.getAvatar())
236                        );
237                        activity.xmppConnectionService.getFileBackend().getAvatarFile(user.getAvatar()).delete();
238                        activity.avatarService().clear(user);
239                        if (user.getContact() != null) activity.avatarService().clear(user.getContact());
240                        user.setAvatar(null);
241                        activity.xmppConnectionService.updateConversationUi();
242                    })
243                    .setNegativeButton(R.string.no, null).show();
244                return true;
245            case R.id.action_mute_participant:
246                if (activity.xmppConnectionService.muteMucUser(user)) {
247                    activity.xmppConnectionService.updateConversationUi();
248                } else {
249                    Toast.makeText(activity, "Failed to mute", Toast.LENGTH_SHORT).show();
250                }
251                return true;
252            case R.id.action_unmute_participant:
253                if (activity.xmppConnectionService.unmuteMucUser(user)) {
254                    activity.xmppConnectionService.updateConversationUi();
255                } else {
256                    Toast.makeText(activity, "Failed to unmute", Toast.LENGTH_SHORT).show();
257                }
258                return true;
259            case R.id.start_conversation:
260                startConversation(user, activity);
261                return true;
262            case R.id.manage_permissions:
263                Pair<CharSequence[], Integer[]> choices = getPermissionsChoices(activity, conversation, user);
264                int[] selected = new int[] { -1 };
265                new AlertDialog.Builder(activity)
266                    .setTitle(activity.getString(R.string.manage_permission_with_nick, UIHelper.getDisplayName(user)))
267                    .setSingleChoiceItems(choices.first, -1, (dialog, whichItem) -> {
268                        selected[0] = whichItem;
269                    })
270                    .setPositiveButton(R.string.action_complete, (dialog, whichButton) -> {
271                        switch (selected[0] >= 0 ? choices.second[selected[0]] : -1) {
272                            case ACTION_BAN:
273                                activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OUTCAST, onAffiliationChanged);
274                                if (user.getRole() != MucOptions.Role.NONE) {
275                                    activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE);
276                                }
277                                maybeModerateRecent(activity, conversation, user);
278                                break;
279                            case ACTION_GRANT_MEMBERSHIP:
280                            case ACTION_REMOVE_ADMIN:
281                            case ACTION_REMOVE_OWNER:
282                                activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged);
283                                break;
284                            case ACTION_GRANT_ADMIN:
285                                activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged);
286                                break;
287                            case ACTION_GRANT_OWNER:
288                                activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OWNER, onAffiliationChanged);
289                                break;
290                            case ACTION_REMOVE_MEMBERSHIP:
291                                activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged);
292                                break;
293                        }
294                    })
295                    .setNeutralButton(R.string.cancel, null).show();
296                return true;
297            case R.id.remove_from_room:
298                removeFromRoom(user, activity, onAffiliationChanged);
299                return true;
300            case R.id.send_private_message:
301                if (activity instanceof ConversationsActivity) {
302                    ConversationFragment conversationFragment = ConversationFragment.get(activity);
303                    if (conversationFragment != null) {
304                        conversationFragment.privateMessageWith(user.getFullJid());
305                        return true;
306                    }
307                }
308                activity.privateMsgInMuc(conversation, user.getName());
309                return true;
310            case R.id.share_contact_details:
311                final var message = new Message(conversation, "/me invites you to chat " + conversation.getAccount().getShareableUri(), conversation.getNextEncryption());
312                Message.configurePrivateMessage(message, user.getFullJid());
313                /* This triggers a gajim bug right now https://dev.gajim.org/gajim/gajim/-/issues/11900
314                final var rosterx = new Element("x", "http://jabber.org/protocol/rosterx");
315                final var ritem = rosterx.addChild("item");
316                ritem.setAttribute("action", "add");
317                ritem.setAttribute("name", conversation.getMucOptions().getSelf().getNick());
318                ritem.setAttribute("jid", conversation.getAccount().getJid().asBareJid().toEscapedString());
319                message.addPayload(rosterx);*/
320                activity.xmppConnectionService.sendMessage(message);
321                return true;
322            case R.id.invite:
323                if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
324                    activity.xmppConnectionService.directInvite(conversation, jid.asBareJid());
325                } else {
326                    activity.xmppConnectionService.invite(conversation, jid);
327                }
328                return true;
329            default:
330                return false;
331        }
332    }
333
334    private static void removeFromRoom(
335            final User user,
336            XmppActivity activity,
337            XmppConnectionService.OnAffiliationChanged onAffiliationChanged) {
338        final Conversation conversation = user.getConversation();
339        if (conversation.getMucOptions().membersOnly()) {
340            activity.xmppConnectionService.changeAffiliationInConference(
341                    conversation,
342                    user.getRealJid(),
343                    MucOptions.Affiliation.NONE,
344                    onAffiliationChanged);
345            if (user.getRole() != MucOptions.Role.NONE) {
346                activity.xmppConnectionService.changeRoleInConference(
347                        conversation, user.getName(), MucOptions.Role.NONE);
348            }
349        } else {
350            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
351            builder.setTitle(R.string.ban_from_conference);
352            String jid = user.getRealJid().asBareJid().toString();
353            SpannableString message =
354                    new SpannableString(
355                            activity.getString(R.string.removing_from_public_conference, jid));
356            int start = message.toString().indexOf(jid);
357            if (start >= 0) {
358                message.setSpan(
359                        new TypefaceSpan("monospace"),
360                        start,
361                        start + jid.length(),
362                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
363            }
364            builder.setMessage(message);
365            builder.setNegativeButton(R.string.cancel, null);
366            builder.setPositiveButton(
367                    R.string.ban_now,
368                    (dialog, which) -> {
369                        activity.xmppConnectionService.changeAffiliationInConference(
370                                conversation,
371                                user.getRealJid(),
372                                MucOptions.Affiliation.OUTCAST,
373                                onAffiliationChanged);
374                        if (user.getRole() != MucOptions.Role.NONE) {
375                            activity.xmppConnectionService.changeRoleInConference(
376                                    conversation, user.getName(), MucOptions.Role.NONE);
377                        }
378                    });
379            builder.create().show();
380        }
381    }
382
383    private static void startConversation(User user, XmppActivity activity) {
384        if (user.getRealJid() != null) {
385            Conversation newConversation =
386                    activity.xmppConnectionService.findOrCreateConversation(
387                            user.getAccount(), user.getRealJid().asBareJid(), false, true);
388            activity.switchToConversation(newConversation);
389        }
390    }
391}