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}