1package eu.siacs.conversations.ui;
2
3import static eu.siacs.conversations.entities.Bookmark.printableValue;
4import static eu.siacs.conversations.utils.StringUtils.changed;
5
6import android.app.Activity;
7import android.content.Context;
8import android.content.Intent;
9import android.content.SharedPreferences;
10import android.content.res.ColorStateList;
11import android.net.Uri;
12import android.os.Build;
13import android.os.Bundle;
14import android.preference.PreferenceManager;
15import android.text.Editable;
16import android.text.SpannableString;
17import android.text.TextWatcher;
18import android.text.method.LinkMovementMethod;
19import android.view.LayoutInflater;
20import android.view.Menu;
21import android.view.MenuItem;
22import android.view.View;
23import android.view.View.OnClickListener;
24import android.view.ViewGroup;
25import android.widget.ArrayAdapter;
26import android.widget.PopupMenu;
27import android.widget.TextView;
28import android.widget.Toast;
29
30import androidx.annotation.NonNull;
31import androidx.annotation.StringRes;
32import androidx.appcompat.app.AlertDialog;
33import androidx.core.view.ViewCompat;
34import androidx.core.content.ContextCompat;
35import androidx.databinding.DataBindingUtil;
36
37import com.cheogram.android.Util;
38
39import com.google.android.material.dialog.MaterialAlertDialogBuilder;
40import com.google.android.material.color.MaterialColors;
41import com.google.common.collect.ImmutableList;
42import com.google.common.primitives.Ints;
43
44import java.util.ArrayList;
45import java.util.Collections;
46import java.util.Comparator;
47import java.util.List;
48import java.util.Map;
49import java.util.concurrent.atomic.AtomicInteger;
50import java.util.stream.Collectors;
51
52import eu.siacs.conversations.Config;
53import com.google.common.base.Strings;
54import com.google.common.collect.ImmutableMap;
55import com.google.common.util.concurrent.FutureCallback;
56import com.google.common.util.concurrent.Futures;
57import de.gultsch.common.Linkify;
58import eu.siacs.conversations.R;
59import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
60import eu.siacs.conversations.databinding.ThreadRowBinding;
61import eu.siacs.conversations.entities.Account;
62import eu.siacs.conversations.entities.Bookmark;
63import eu.siacs.conversations.entities.Contact;
64import eu.siacs.conversations.entities.Conversation;
65import eu.siacs.conversations.entities.ListItem;
66import eu.siacs.conversations.entities.MucOptions;
67import eu.siacs.conversations.entities.MucOptions.User;
68import eu.siacs.conversations.services.XmppConnectionService;
69import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
70import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
71import eu.siacs.conversations.ui.adapter.MediaAdapter;
72import eu.siacs.conversations.ui.adapter.UserPreviewAdapter;
73import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
74import eu.siacs.conversations.ui.text.FixedURLSpan;
75import eu.siacs.conversations.ui.util.Attachment;
76import eu.siacs.conversations.ui.util.AvatarWorkerTask;
77import eu.siacs.conversations.ui.util.GridManager;
78import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
79import eu.siacs.conversations.ui.util.MucConfiguration;
80import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
81import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
82import eu.siacs.conversations.utils.AccountUtils;
83import eu.siacs.conversations.utils.Compatibility;
84import eu.siacs.conversations.utils.StylingHelper;
85import eu.siacs.conversations.utils.UIHelper;
86import eu.siacs.conversations.utils.XmppUri;
87import eu.siacs.conversations.utils.XEP0392Helper;
88import eu.siacs.conversations.xmpp.Jid;
89import eu.siacs.conversations.xmpp.XmppConnection;
90
91import eu.siacs.conversations.xmpp.manager.BookmarkManager;
92import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
93import im.conversations.android.xmpp.model.muc.Affiliation;
94import im.conversations.android.xmpp.model.muc.Role;
95import java.util.Collections;
96import java.util.List;
97import java.util.concurrent.atomic.AtomicInteger;
98import me.drakeet.support.toast.ToastCompat;
99
100public class ConferenceDetailsActivity extends XmppActivity
101 implements OnConversationUpdate,
102 OnMucRosterUpdate,
103 XmppConnectionService.OnAffiliationChanged,
104 TextWatcher,
105 OnMediaLoaded {
106 public static final String ACTION_VIEW_MUC = "view_muc";
107
108 private Conversation mConversation;
109 private ActivityMucDetailsBinding binding;
110 private MediaAdapter mMediaAdapter;
111 private UserPreviewAdapter mUserPreviewAdapter;
112 private String uuid = null;
113
114 private boolean mAdvancedMode = false;
115 private boolean showDynamicTags = true;
116
117 private FutureCallback<Void> renameCallback =
118 new FutureCallback<Void>() {
119 @Override
120 public void onSuccess(Void result) {
121 displayToast(getString(R.string.your_nick_has_been_changed));
122 updateView();
123 }
124
125 @Override
126 public void onFailure(Throwable t) {
127
128 // TODO check for NickInUseException and NickInvalid exception
129
130 }
131 };
132
133 public static void open(final Activity activity, final Conversation conversation) {
134 Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
135 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
136 intent.putExtra("uuid", conversation.getUuid());
137 activity.startActivity(intent);
138 }
139
140 private final OnClickListener mNotifyStatusClickListener =
141 new OnClickListener() {
142 @Override
143 public void onClick(View v) {
144 final MaterialAlertDialogBuilder builder =
145 new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
146 builder.setTitle(R.string.pref_notification_settings);
147 String[] choices = {
148 getString(R.string.notify_on_all_messages),
149 getString(R.string.notify_only_when_highlighted),
150 getString(R.string.notify_only_when_highlighted_or_replied),
151 getString(R.string.notify_never)
152 };
153 final AtomicInteger choice;
154 if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0)
155 == Long.MAX_VALUE) {
156 choice = new AtomicInteger(3);
157 } else {
158 choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1));
159 }
160 builder.setSingleChoiceItems(
161 choices, choice.get(), (dialog, which) -> choice.set(which));
162 builder.setNegativeButton(R.string.cancel, null);
163 builder.setPositiveButton(
164 R.string.ok,
165 (dialog, which) -> {
166 if (choice.get() == 3) {
167 mConversation.setMutedTill(Long.MAX_VALUE);
168 } else {
169 mConversation.setMutedTill(0);
170 mConversation.setAttribute(
171 Conversation.ATTRIBUTE_ALWAYS_NOTIFY,
172 String.valueOf(choice.get() == 0));
173 mConversation.setAttribute(
174 Conversation.ATTRIBUTE_NOTIFY_REPLIES,
175 String.valueOf(choice.get() == 2));
176 }
177 xmppConnectionService.updateConversation(mConversation);
178 updateView();
179 });
180 builder.create().show();
181 }
182 };
183
184 private final FutureCallback<Void> onConfigurationPushed =
185 new FutureCallback<Void>() {
186
187 @Override
188 public void onSuccess(Void result) {
189 displayToast(getString(R.string.modified_conference_options));
190 }
191
192 @Override
193 public void onFailure(Throwable t) {
194 displayToast(getString(R.string.could_not_modify_conference_options));
195 }
196 };
197
198 private final OnClickListener mChangeConferenceSettings =
199 new OnClickListener() {
200 @Override
201 public void onClick(View v) {
202 final MucOptions mucOptions = mConversation.getMucOptions();
203 final MaterialAlertDialogBuilder builder =
204 new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
205 MucConfiguration configuration =
206 MucConfiguration.get(
207 ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
208 builder.setTitle(configuration.title);
209 final boolean[] values = configuration.values;
210 builder.setMultiChoiceItems(
211 configuration.names,
212 values,
213 (dialog, which, isChecked) -> values[which] = isChecked);
214 builder.setNegativeButton(R.string.cancel, null);
215 builder.setPositiveButton(
216 R.string.confirm,
217 (dialog, which) -> {
218 final var options = configuration.toBundle(values);
219 final var future =
220 mConversation
221 .getAccount()
222 .getXmppConnection()
223 .getManager(MultiUserChatManager.class)
224 .pushConfiguration(mConversation, options);
225 Futures.addCallback(
226 future,
227 onConfigurationPushed,
228 ContextCompat.getMainExecutor(getApplication()));
229 });
230 builder.create().show();
231 }
232 };
233
234 @Override
235 public void onConversationUpdate() {
236 refreshUi();
237 }
238
239 @Override
240 public void onMucRosterUpdate() {
241 refreshUi();
242 }
243
244 @Override
245 protected void refreshUiReal() {
246 updateView();
247 }
248
249 @Override
250 protected void onCreate(Bundle savedInstanceState) {
251 super.onCreate(savedInstanceState);
252 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details);
253 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
254 showDynamicTags = preferences.getBoolean("show_dynamic_tags", getResources().getBoolean(R.bool.show_dynamic_tags));
255 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
256 this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
257 setSupportActionBar(binding.toolbar);
258 configureActionBar(getSupportActionBar());
259 this.binding.editNickButton.setOnClickListener(
260 v ->
261 quickEdit(
262 mConversation.getMucOptions().getActualNick(),
263 R.string.nickname,
264 value -> {
265 if (mConversation.getMucOptions().createJoinJid(value)
266 == null) {
267 return getString(R.string.invalid_muc_nick);
268 }
269 final var future =
270 mConversation
271 .getAccount()
272 .getXmppConnection()
273 .getManager(MultiUserChatManager.class)
274 .changeUsername(mConversation, value);
275 Futures.addCallback(
276 future,
277 renameCallback,
278 ContextCompat.getMainExecutor(this));
279 return null;
280 }));
281 this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
282 this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
283 this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
284 this.binding.yourPhoto.setOnClickListener(
285 v -> {
286 final MucOptions mucOptions = mConversation.getMucOptions();
287 if (!mucOptions.hasVCards()) {
288 Toast.makeText(
289 this,
290 R.string.host_does_not_support_group_chat_avatars,
291 Toast.LENGTH_SHORT)
292 .show();
293 return;
294 }
295 if (!mucOptions.getSelf().ranks(Affiliation.OWNER)) {
296 Toast.makeText(
297 this,
298 R.string.only_the_owner_can_change_group_chat_avatar,
299 Toast.LENGTH_SHORT)
300 .show();
301 return;
302 }
303 final Intent intent =
304 new Intent(this, PublishGroupChatProfilePictureActivity.class);
305 intent.putExtra("uuid", mConversation.getUuid());
306 startActivity(intent);
307 });
308 this.binding.yourPhoto.setOnLongClickListener(v -> {
309 PopupMenu popupMenu = new PopupMenu(this, v);
310 popupMenu.inflate(R.menu.conference_photo);
311 popupMenu.setOnMenuItemClickListener(menuItem -> {
312 switch (menuItem.getItemId()) {
313 case R.id.action_block_avatar:
314 new AlertDialog.Builder(this)
315 .setTitle(R.string.block_media)
316 .setMessage("Do you really want to block this avatar?")
317 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
318 xmppConnectionService.blockMedia(xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatar()));
319 xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatar()).delete();
320 avatarService().clear(mConversation);
321 mConversation.getContact().setAvatar(null);
322 xmppConnectionService.updateConversationUi();
323 })
324 .setNegativeButton(R.string.no, null).show();
325 return true;
326 }
327 return true;
328 });
329 popupMenu.show();
330 return true;
331 });
332 this.binding.editMucNameButton.setContentDescription(
333 getString(R.string.edit_name_and_topic));
334 this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
335 this.binding.mucEditTitle.addTextChangedListener(this);
336 this.binding.mucEditSubject.addTextChangedListener(this);
337 //this.binding.mucEditSubject.addTextChangedListener(
338 // new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
339 this.binding.editTags.addTextChangedListener(this);
340 this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
341 this.mUserPreviewAdapter = new UserPreviewAdapter();
342 this.binding.media.setAdapter(mMediaAdapter);
343 this.binding.users.setAdapter(mUserPreviewAdapter);
344 GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
345 GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size);
346 this.binding.recentThreads.setOnItemClickListener((a0, v, pos, a3) -> {
347 final Conversation.Thread thread = (Conversation.Thread) binding.recentThreads.getAdapter().getItem(pos);
348 switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId());
349 });
350 this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
351 this.binding.showUsers.setOnClickListener(
352 v -> {
353 Intent intent = new Intent(this, MucUsersActivity.class);
354 intent.putExtra("uuid", mConversation.getUuid());
355 startActivity(intent);
356 });
357 this.binding.relatedMucs.setOnClickListener(v -> {
358 final Intent intent = new Intent(this, ChannelDiscoveryActivity.class);
359 intent.putExtra("services", new String[]{ mConversation.getJid().getDomain().toString(), mConversation.getAccount().getJid().toString() });
360 startActivity(intent);
361 });
362 }
363
364 @Override
365 public void onStart() {
366 super.onStart();
367 binding.mediaWrapper.setVisibility(
368 Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
369 }
370
371 @Override
372 public boolean onOptionsItemSelected(MenuItem menuItem) {
373 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
374 return false;
375 }
376 switch (menuItem.getItemId()) {
377 case android.R.id.home:
378 finish();
379 break;
380 case R.id.action_share_http:
381 shareLink(true);
382 break;
383 case R.id.action_share_uri:
384 shareLink(false);
385 break;
386 case R.id.action_save_as_bookmark:
387 saveAsBookmark();
388 break;
389 case R.id.action_destroy_room:
390 destroyRoom();
391 break;
392 case R.id.action_advanced_mode:
393 this.mAdvancedMode = !menuItem.isChecked();
394 menuItem.setChecked(this.mAdvancedMode);
395 getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
396 final boolean online =
397 mConversation != null && mConversation.getMucOptions().online();
398 this.binding.mucInfoMore.setVisibility(
399 this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
400 invalidateOptionsMenu();
401 updateView();
402 break;
403 case R.id.action_custom_notifications:
404 if (mConversation != null) {
405 configureCustomNotifications(mConversation);
406 }
407 break;
408 }
409 return super.onOptionsItemSelected(menuItem);
410 }
411
412 private void configureCustomNotifications(final Conversation conversation) {
413 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
414 || conversation.getMode() != Conversation.MODE_MULTI) {
415 return;
416 }
417 final var shortcut =
418 xmppConnectionService
419 .getShortcutService()
420 .getShortcutInfo(conversation.getMucOptions());
421 configureCustomNotification(shortcut);
422 }
423
424 @Override
425 public boolean onContextItemSelected(@NonNull final MenuItem item) {
426 final User user = mUserPreviewAdapter.getSelectedUser();
427 if (user == null) {
428 Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show();
429 return true;
430 }
431 if (!MucDetailsContextMenuHelper.onContextItemSelected(
432 item, mUserPreviewAdapter.getSelectedUser(), this)) {
433 return super.onContextItemSelected(item);
434 }
435 return true;
436 }
437
438 public void onMucEditButtonClicked(View v) {
439 if (this.binding.mucEditor.getVisibility() == View.GONE) {
440 final MucOptions mucOptions = mConversation.getMucOptions();
441 this.binding.mucEditor.setVisibility(View.VISIBLE);
442 this.binding.mucDisplay.setVisibility(View.GONE);
443 this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
444 this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
445 final String name = mucOptions.getName();
446 this.binding.mucEditTitle.setText("");
447 final boolean owner = mucOptions.getSelf().ranks(Affiliation.OWNER);
448 if (owner || printableValue(name)) {
449 this.binding.mucEditTitle.setVisibility(View.VISIBLE);
450 if (name != null) {
451 this.binding.mucEditTitle.append(name);
452 }
453 } else {
454 this.binding.mucEditTitle.setVisibility(View.GONE);
455 }
456 this.binding.mucEditTitle.setEnabled(owner);
457 final String subject = mucOptions.getSubject();
458 this.binding.mucEditSubject.setText("");
459 if (subject != null) {
460 this.binding.mucEditSubject.append(subject);
461 }
462 this.binding.mucEditSubject.setEnabled(mucOptions.canChangeSubject());
463 if (!owner) {
464 this.binding.mucEditSubject.requestFocus();
465 }
466
467 final Bookmark bookmark = mConversation.getBookmark();
468 if (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2() && showDynamicTags) {
469 for (final ListItem.Tag group : bookmark.getGroupTags()) {
470 binding.editTags.addObjectSync(group);
471 }
472 ArrayList<ListItem.Tag> tags = new ArrayList<>();
473 for (final Account account : xmppConnectionService.getAccounts()) {
474 for (Contact contact : account.getRoster().getContacts()) {
475 tags.addAll(contact.getTags(this));
476 }
477 for (Bookmark bmark : account.getBookmarks()) {
478 tags.addAll(bmark.getTags(this));
479 }
480 }
481 Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
482 sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
483
484 ArrayAdapter<ListItem.Tag> adapter = new ArrayAdapter<>(
485 this,
486 android.R.layout.simple_list_item_1,
487 tags.stream()
488 .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
489 .entrySet().stream()
490 .sorted(sortTagsBy)
491 .map(e -> e.getKey()).collect(Collectors.toList())
492 );
493 binding.editTags.setAdapter(adapter);
494 this.binding.editTags.setVisibility(View.VISIBLE);
495 } else {
496 this.binding.editTags.setVisibility(View.GONE);
497 }
498 } else {
499 String subject =
500 this.binding.mucEditSubject.isEnabled()
501 ? this.binding.mucEditSubject.getEditableText().toString().trim()
502 : null;
503 String name =
504 this.binding.mucEditTitle.isEnabled()
505 ? this.binding.mucEditTitle.getEditableText().toString().trim()
506 : null;
507 onMucInfoUpdated(subject, name);
508
509 final Bookmark bookmark = mConversation.getBookmark();
510 if (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2()) {
511 bookmark.setGroups(binding.editTags.getObjects().stream().map(tag -> tag.getName()).collect(Collectors.toList()));
512 xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
513 }
514
515 SoftKeyboardUtils.hideSoftKeyboard(this);
516 hideEditor();
517 updateView();
518 }
519 }
520
521 private void hideEditor() {
522 this.binding.mucEditor.setVisibility(View.GONE);
523 this.binding.mucDisplay.setVisibility(View.VISIBLE);
524 this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
525 this.binding.editMucNameButton.setContentDescription(
526 getString(R.string.edit_name_and_topic));
527 }
528
529 private void onMucInfoUpdated(String subject, String name) {
530 final var account = mConversation.getAccount();
531 final MucOptions mucOptions = mConversation.getMucOptions();
532 if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
533 xmppConnectionService.pushSubjectToConference(mConversation, subject);
534 }
535 if (mucOptions.getSelf().ranks(Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
536 final var options =
537 new ImmutableMap.Builder<String, Object>()
538 .put("muc#roomconfig_persistentroom", true)
539 .put("muc#roomconfig_roomname", Strings.nullToEmpty(name))
540 .build();
541 final var future =
542 account.getXmppConnection()
543 .getManager(MultiUserChatManager.class)
544 .pushConfiguration(mConversation, options);
545 Futures.addCallback(
546 future, onConfigurationPushed, ContextCompat.getMainExecutor(getApplication()));
547 }
548 }
549
550 @Override
551 protected String getShareableUri(boolean http) {
552 if (mConversation != null) {
553 if (http) {
554 return "https://conversations.im/j/"
555 + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toString());
556 } else {
557 return "xmpp:" + Uri.encode(mConversation.getJid().asBareJid().toString(), "@/+") + "?join";
558 }
559 } else {
560 return null;
561 }
562 }
563
564 @Override
565 public boolean onPrepareOptionsMenu(final Menu menu) {
566 final MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
567 final MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
568 final MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room);
569 menuItemAdvancedMode.setChecked(mAdvancedMode);
570 if (mConversation == null) {
571 return true;
572 }
573 menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
574 menuItemDestroyRoom.setVisible(
575 mConversation.getMucOptions().getSelf().ranks(Affiliation.OWNER));
576 return true;
577 }
578
579 @Override
580 public boolean onCreateOptionsMenu(final Menu menu) {
581 final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
582 getMenuInflater().inflate(R.menu.muc_details, menu);
583 final MenuItem share = menu.findItem(R.id.action_share);
584 share.setVisible(!groupChat);
585 final MenuItem destroy = menu.findItem(R.id.action_destroy_room);
586 destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
587 AccountUtils.showHideMenuItems(menu);
588 final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
589 customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
590 return super.onCreateOptionsMenu(menu);
591 }
592
593 @Override
594 public void onMediaLoaded(final List<Attachment> attachments) {
595 runOnUiThread(
596 () -> {
597 final int limit = GridManager.getCurrentColumnCount(binding.media);
598 mMediaAdapter.setAttachments(
599 attachments.subList(0, Math.min(limit, attachments.size())));
600 binding.mediaWrapper.setVisibility(
601 attachments.isEmpty() ? View.GONE : View.VISIBLE);
602 });
603 }
604
605 protected void saveAsBookmark() {
606 final var account = mConversation.getAccount();
607 account.getXmppConnection()
608 .getManager(BookmarkManager.class)
609 .save(mConversation, mConversation.getMucOptions().getName());
610 }
611
612 protected void destroyRoom() {
613 final var destroyCallBack =
614 new FutureCallback<Void>() {
615
616 @Override
617 public void onSuccess(Void result) {
618 finish();
619 }
620
621 @Override
622 public void onFailure(Throwable t) {
623 final boolean groupChat =
624 mConversation != null && mConversation.isPrivateAndNonAnonymous();
625 // TODO show toast directly
626 displayToast(
627 getString(
628 groupChat
629 ? R.string.could_not_destroy_room
630 : R.string.could_not_destroy_channel));
631 }
632 };
633 final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
634 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
635 builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
636 builder.setMessage(
637 groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
638 builder.setPositiveButton(
639 R.string.ok,
640 (dialog, which) -> {
641 final var future = xmppConnectionService.destroyRoom(mConversation);
642 Futures.addCallback(
643 future,
644 destroyCallBack,
645 ContextCompat.getMainExecutor(getApplication()));
646 });
647 builder.setNegativeButton(R.string.cancel, null);
648 final AlertDialog dialog = builder.create();
649 dialog.setCanceledOnTouchOutside(false);
650 dialog.show();
651 }
652
653 @Override
654 protected void onBackendConnected() {
655 if (mPendingConferenceInvite != null) {
656 mPendingConferenceInvite.execute(this);
657 mPendingConferenceInvite = null;
658 }
659 if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
660 this.uuid = getIntent().getExtras().getString("uuid");
661 }
662 if (uuid != null) {
663 this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
664 if (this.mConversation != null) {
665 if (Compatibility.hasStoragePermission(this)) {
666 final int limit = GridManager.getCurrentColumnCount(this.binding.media);
667 xmppConnectionService.getAttachments(this.mConversation, limit, this);
668 this.binding.showMedia.setOnClickListener(
669 (v) -> MediaBrowserActivity.launch(this, mConversation));
670 }
671
672 binding.storeInCache.setChecked(mConversation.storeInCache());
673 binding.storeInCache.setOnCheckedChangeListener((v, checked) -> {
674 mConversation.setStoreInCache(checked);
675 xmppConnectionService.updateConversation(mConversation);
676 });
677
678 updateView();
679 }
680 }
681 }
682
683 @Override
684 public void onBackPressed() {
685 if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
686 hideEditor();
687 } else {
688 super.onBackPressed();
689 }
690 }
691
692 private void updateView() {
693 invalidateOptionsMenu();
694 if (mConversation == null) {
695 return;
696 }
697 final MucOptions mucOptions = mConversation.getMucOptions();
698 final User self = mucOptions.getSelf();
699 final String account = mConversation.getAccount().getJid().asBareJid().toString();
700 setTitle(
701 mucOptions.isPrivateAndNonAnonymous()
702 ? R.string.action_muc_details
703 : R.string.channel_details);
704 final Bookmark bookmark = mConversation.getBookmark();
705 final XmppConnection connection = mConversation.getAccount().getXmppConnection();
706 this.binding.editMucNameButton.setVisibility((self.ranks(Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE);
707 this.binding.detailsAccount.setText(getString(R.string.using_account, account));
708 this.binding.truejid.setVisibility(View.GONE);
709 if (mConversation.isPrivateAndNonAnonymous()) {
710 this.binding.jid.setText(
711 getString(R.string.hosted_on, mConversation.getJid().getDomain()));
712 this.binding.truejid.setText(mConversation.getJid().asBareJid().toString());
713 if (mAdvancedMode) this.binding.truejid.setVisibility(View.VISIBLE);
714 } else {
715 this.binding.jid.setText(mConversation.getJid().asBareJid().toString());
716 }
717 AvatarWorkerTask.loadAvatar(
718 mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
719 String roomName = mucOptions.getName();
720 String subject = mucOptions.getSubject();
721 final boolean hasTitle;
722 if (printableValue(roomName)) {
723 this.binding.mucTitle.setText(roomName);
724 this.binding.mucTitle.setVisibility(View.VISIBLE);
725 hasTitle = true;
726 } else if (!printableValue(subject)) {
727 this.binding.mucTitle.setText(mConversation.getName());
728 hasTitle = true;
729 this.binding.mucTitle.setVisibility(View.VISIBLE);
730 } else {
731 hasTitle = false;
732 this.binding.mucTitle.setVisibility(View.GONE);
733 }
734 if (printableValue(subject)) {
735 final var spannable = new SpannableString(subject);
736 StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
737 Linkify.addLinks(spannable);
738 FixedURLSpan.fix(spannable);
739 this.binding.mucSubject.setText(spannable);
740 this.binding.mucSubject.setTextAppearance(
741 subject.length() > (hasTitle ? 128 : 196)
742 ? com.google.android.material.R.style
743 .TextAppearance_Material3_BodyMedium
744 : com.google.android.material.R.style
745 .TextAppearance_Material3_BodyLarge);
746 this.binding.mucSubject.setVisibility(View.VISIBLE);
747 this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
748 } else {
749 this.binding.mucSubject.setVisibility(View.GONE);
750 }
751 this.binding.mucYourNick.setText(mucOptions.getActualNick());
752 if (mucOptions.online()) {
753 this.binding.usersWrapper.setVisibility(View.VISIBLE);
754 this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
755 this.binding.mucRole.setVisibility(View.VISIBLE);
756 this.binding.mucRole.setText(getStatus(self));
757 if (mucOptions.getSelf().ranks(Affiliation.OWNER)) {
758 this.binding.mucSettings.setVisibility(View.VISIBLE);
759 this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
760 } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
761 this.binding.mucSettings.setVisibility(View.VISIBLE);
762 this.binding.mucConferenceType.setText(
763 R.string.group_chat_will_make_your_jabber_id_public);
764 } else {
765 this.binding.mucSettings.setVisibility(View.GONE);
766 }
767 if (mucOptions.mamSupport()) {
768 this.binding.mucInfoMam.setText(R.string.server_info_available);
769 } else {
770 this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
771 }
772 if (self.ranks(Affiliation.OWNER)) {
773 this.binding.changeConferenceButton.setVisibility(View.VISIBLE);
774 } else {
775 this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
776 }
777 } else {
778 this.binding.usersWrapper.setVisibility(View.GONE);
779 this.binding.mucInfoMore.setVisibility(View.GONE);
780 this.binding.mucSettings.setVisibility(View.GONE);
781 }
782
783 final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
784 if (mutedTill == Long.MAX_VALUE) {
785 this.binding.notificationStatusText.setText(R.string.notify_never);
786 this.binding.notificationStatusButton.setImageResource(
787 R.drawable.ic_notifications_off_24dp);
788 } else if (System.currentTimeMillis() < mutedTill) {
789 this.binding.notificationStatusText.setText(R.string.notify_paused);
790 this.binding.notificationStatusButton.setImageResource(
791 R.drawable.ic_notifications_paused_24dp);
792 } else if (mConversation.alwaysNotify()) {
793 this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
794 this.binding.notificationStatusButton.setImageResource(
795 R.drawable.ic_notifications_24dp);
796 } else if (mConversation.notifyReplies()) {
797 this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted_or_replied);
798 this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
799 } else {
800 this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
801 this.binding.notificationStatusButton.setImageResource(
802 R.drawable.ic_notifications_none_24dp);
803 }
804 final List<User> users = mucOptions.getUsers();
805 Collections.sort(
806 users,
807 (a, b) -> {
808 if (b.outranks(a.getAffiliation())) {
809 return 1;
810 } else if (a.outranks(b.getAffiliation())) {
811 return -1;
812 } else {
813 if (a.getAvatar() != null && b.getAvatar() == null) {
814 return -1;
815 } else if (a.getAvatar() == null && b.getAvatar() != null) {
816 return 1;
817 } else {
818 return a.getComparableName().compareToIgnoreCase(b.getComparableName());
819 }
820 }
821 });
822 this.binding.users.post(
823 () -> {
824 final var list =
825 MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users));
826 this.mUserPreviewAdapter.submitList(list);
827 });
828 this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
829 this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().ranks(Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE);
830 this.binding.showUsers.setText(
831 getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
832 this.binding.usersWrapper.setVisibility(
833 users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
834 if (users.size() == 0) {
835 this.binding.noUsersHints.setText(
836 mucOptions.isPrivateAndNonAnonymous()
837 ? R.string.no_users_hint_group_chat
838 : R.string.no_users_hint_channel);
839 this.binding.noUsersHints.setVisibility(View.VISIBLE);
840 } else {
841 this.binding.noUsersHints.setVisibility(View.GONE);
842 }
843
844 if (bookmark == null) {
845 binding.tags.setVisibility(View.GONE);
846 return;
847 }
848
849 final List<Conversation.Thread> recentThreads = mConversation.recentThreads();
850 if (recentThreads.isEmpty()) {
851 this.binding.recentThreadsWrapper.setVisibility(View.GONE);
852 } else {
853 final ThreadAdapter threads = new ThreadAdapter();
854 threads.addAll(recentThreads);
855 this.binding.recentThreads.setAdapter(threads);
856 this.binding.recentThreadsWrapper.setVisibility(View.VISIBLE);
857 Util.justifyListViewHeightBasedOnChildren(binding.recentThreads);
858 }
859
860 final List<ListItem.Tag> tagList = bookmark.getTags(this);
861 if (tagList.isEmpty() || !this.showDynamicTags) {
862 binding.tags.setVisibility(View.GONE);
863 } else {
864 final LayoutInflater inflater = getLayoutInflater();
865 binding.tags.setVisibility(View.VISIBLE);
866 binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
867 final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
868 for (final ListItem.Tag tag : tagList) {
869 final String name = tag.getName();
870 final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
871 tv.setText(name);
872 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
873 final int id = ViewCompat.generateViewId();
874 tv.setId(id);
875 viewIdBuilder.add(id);
876 binding.tags.addView(tv);
877 }
878 binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
879 }
880 }
881
882 public static String getStatus(Context context, User user, final boolean advanced) {
883 var hats = context.getString(roleToStringRes(user.getRole()));
884 for (final var hat : user.getHats()) {
885 hats += ", " + hat;
886 }
887 return String.format(
888 "%s, %s",
889 context.getString(affiliationToStringRes(user.getAffiliation())), hats);
890 }
891
892 public static @StringRes int affiliationToStringRes(final Affiliation affiliation) {
893 return switch (affiliation) {
894 case OWNER -> R.string.owner;
895 case ADMIN -> R.string.admin;
896 case MEMBER -> R.string.member;
897 case NONE -> R.string.no_affiliation;
898 case OUTCAST -> R.string.outcast;
899 };
900 }
901
902 public static @StringRes int roleToStringRes(final Role role) {
903 return switch (role) {
904 case MODERATOR -> R.string.moderator;
905 case VISITOR -> R.string.visitor;
906 case PARTICIPANT -> R.string.participant;
907 case NONE -> R.string.no_role;
908 };
909 }
910
911 private String getStatus(User user) {
912 return getStatus(this, user, mAdvancedMode);
913 }
914
915 @Override
916 public void onAffiliationChangedSuccessful(Jid jid) {
917 refreshUi();
918 }
919
920 @Override
921 public void onAffiliationChangeFailed(Jid jid, int resId) {
922 displayToast(getString(resId, jid.asBareJid().toString()));
923 }
924
925 private void displayToast(final String msg) {
926 runOnUiThread(
927 () -> {
928 if (isFinishing()) {
929 return;
930 }
931 ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
932 });
933 }
934
935 @Override
936 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
937
938 @Override
939 public void onTextChanged(CharSequence s, int start, int before, int count) {}
940
941 @Override
942 public void afterTextChanged(Editable s) {
943 if (mConversation == null) {
944 return;
945 }
946 final MucOptions mucOptions = mConversation.getMucOptions();
947 if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
948 boolean subjectChanged =
949 changed(
950 binding.mucEditSubject.getEditableText().toString(),
951 mucOptions.getSubject());
952 boolean nameChanged =
953 changed(
954 binding.mucEditTitle.getEditableText().toString(),
955 mucOptions.getName());
956 final Bookmark bookmark = mConversation.getBookmark();
957 if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) {
958 this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
959 this.binding.editMucNameButton.setContentDescription(getString(R.string.save));
960 } else {
961 this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
962 this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
963 }
964 }
965 }
966
967 class ThreadAdapter extends ArrayAdapter<Conversation.Thread> {
968 ThreadAdapter() { super(ConferenceDetailsActivity.this, 0); }
969
970 @Override
971 public View getView(int position, View view, @NonNull ViewGroup parent) {
972 final ThreadRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.thread_row, parent, false);
973 final Conversation.Thread item = getItem(position);
974
975 binding.threadIdenticon.setColor(UIHelper.getColorForName(item.getThreadId()));
976 binding.threadIdenticon.setHash(UIHelper.identiconHash(item.getThreadId()));
977
978 binding.threadSubject.setText(item.getDisplay());
979
980 return binding.getRoot();
981 }
982 }
983}