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