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().toEscapedString(), mConversation.getAccount().getJid().toEscapedString() });
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(
522 mConversation.getJid().asBareJid().toEscapedString());
523 } else {
524 return "xmpp:" + Uri.encode(mConversation.getJid().asBareJid().toEscapedString(), "@/+") + "?join";
525 }
526 } else {
527 return null;
528 }
529 }
530
531 @Override
532 public boolean onPrepareOptionsMenu(final Menu menu) {
533 final MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
534 final MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
535 final MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room);
536 menuItemAdvancedMode.setChecked(mAdvancedMode);
537 if (mConversation == null) {
538 return true;
539 }
540 menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
541 menuItemDestroyRoom.setVisible(
542 mConversation
543 .getMucOptions()
544 .getSelf()
545 .getAffiliation()
546 .ranks(MucOptions.Affiliation.OWNER));
547 return true;
548 }
549
550 @Override
551 public boolean onCreateOptionsMenu(final Menu menu) {
552 final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
553 getMenuInflater().inflate(R.menu.muc_details, menu);
554 final MenuItem share = menu.findItem(R.id.action_share);
555 share.setVisible(!groupChat);
556 final MenuItem destroy = menu.findItem(R.id.action_destroy_room);
557 destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
558 AccountUtils.showHideMenuItems(menu);
559 final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
560 customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
561 return super.onCreateOptionsMenu(menu);
562 }
563
564 @Override
565 public void onMediaLoaded(final List<Attachment> attachments) {
566 runOnUiThread(
567 () -> {
568 final int limit = GridManager.getCurrentColumnCount(binding.media);
569 mMediaAdapter.setAttachments(
570 attachments.subList(0, Math.min(limit, attachments.size())));
571 binding.mediaWrapper.setVisibility(
572 attachments.isEmpty() ? View.GONE : View.VISIBLE);
573 });
574 }
575
576 protected void saveAsBookmark() {
577 xmppConnectionService.saveConversationAsBookmark(
578 mConversation, mConversation.getMucOptions().getName());
579 }
580
581 protected void destroyRoom() {
582 final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
583 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
584 builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
585 builder.setMessage(
586 groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
587 builder.setPositiveButton(
588 R.string.ok,
589 (dialog, which) -> {
590 xmppConnectionService.destroyRoom(
591 mConversation, ConferenceDetailsActivity.this);
592 });
593 builder.setNegativeButton(R.string.cancel, null);
594 final AlertDialog dialog = builder.create();
595 dialog.setCanceledOnTouchOutside(false);
596 dialog.show();
597 }
598
599 @Override
600 protected void onBackendConnected() {
601 if (mPendingConferenceInvite != null) {
602 mPendingConferenceInvite.execute(this);
603 mPendingConferenceInvite = null;
604 }
605 if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
606 this.uuid = getIntent().getExtras().getString("uuid");
607 }
608 if (uuid != null) {
609 this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
610 if (this.mConversation != null) {
611 if (Compatibility.hasStoragePermission(this)) {
612 final int limit = GridManager.getCurrentColumnCount(this.binding.media);
613 xmppConnectionService.getAttachments(this.mConversation, limit, this);
614 this.binding.showMedia.setOnClickListener(
615 (v) -> MediaBrowserActivity.launch(this, mConversation));
616 }
617
618 binding.storeInCache.setChecked(mConversation.storeInCache());
619 binding.storeInCache.setOnCheckedChangeListener((v, checked) -> {
620 mConversation.setStoreInCache(checked);
621 xmppConnectionService.updateConversation(mConversation);
622 });
623
624 updateView();
625 }
626 }
627 }
628
629 @Override
630 public void onBackPressed() {
631 if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
632 hideEditor();
633 } else {
634 super.onBackPressed();
635 }
636 }
637
638 private void updateView() {
639 invalidateOptionsMenu();
640 if (mConversation == null) {
641 return;
642 }
643 final MucOptions mucOptions = mConversation.getMucOptions();
644 final User self = mucOptions.getSelf();
645 final String account = mConversation.getAccount().getJid().asBareJid().toEscapedString();
646 setTitle(
647 mucOptions.isPrivateAndNonAnonymous()
648 ? R.string.action_muc_details
649 : R.string.channel_details);
650 final Bookmark bookmark = mConversation.getBookmark();
651 final XmppConnection connection = mConversation.getAccount().getXmppConnection();
652 this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE);
653 this.binding.detailsAccount.setText(getString(R.string.using_account, account));
654 this.binding.truejid.setVisibility(View.GONE);
655 if (mConversation.isPrivateAndNonAnonymous()) {
656 this.binding.jid.setText(
657 getString(R.string.hosted_on, mConversation.getJid().getDomain()));
658 this.binding.truejid.setText(mConversation.getJid().asBareJid().toEscapedString());
659 if (mAdvancedMode) this.binding.truejid.setVisibility(View.VISIBLE);
660 } else {
661 this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString());
662 }
663 AvatarWorkerTask.loadAvatar(
664 mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
665 String roomName = mucOptions.getName();
666 String subject = mucOptions.getSubject();
667 final boolean hasTitle;
668 if (printableValue(roomName)) {
669 this.binding.mucTitle.setText(roomName);
670 this.binding.mucTitle.setVisibility(View.VISIBLE);
671 hasTitle = true;
672 } else if (!printableValue(subject)) {
673 this.binding.mucTitle.setText(mConversation.getName());
674 hasTitle = true;
675 this.binding.mucTitle.setVisibility(View.VISIBLE);
676 } else {
677 hasTitle = false;
678 this.binding.mucTitle.setVisibility(View.GONE);
679 }
680 if (printableValue(subject)) {
681 SpannableStringBuilder spannable = new SpannableStringBuilder(subject);
682 StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
683 MyLinkify.addLinks(spannable, false);
684 this.binding.mucSubject.setText(spannable);
685 this.binding.mucSubject.setTextAppearance(
686 subject.length() > (hasTitle ? 128 : 196)
687 ? com.google.android.material.R.style
688 .TextAppearance_Material3_BodyMedium
689 : com.google.android.material.R.style
690 .TextAppearance_Material3_BodyLarge);
691 this.binding.mucSubject.setAutoLinkMask(0);
692 this.binding.mucSubject.setVisibility(View.VISIBLE);
693 this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
694 } else {
695 this.binding.mucSubject.setVisibility(View.GONE);
696 }
697 this.binding.mucYourNick.setText(mucOptions.getActualNick());
698 if (mucOptions.online()) {
699 this.binding.usersWrapper.setVisibility(View.VISIBLE);
700 this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
701 this.binding.mucRole.setVisibility(View.VISIBLE);
702 this.binding.mucRole.setText(getStatus(self));
703 if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
704 this.binding.mucSettings.setVisibility(View.VISIBLE);
705 this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
706 } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
707 this.binding.mucSettings.setVisibility(View.VISIBLE);
708 this.binding.mucConferenceType.setText(
709 R.string.group_chat_will_make_your_jabber_id_public);
710 } else {
711 this.binding.mucSettings.setVisibility(View.GONE);
712 }
713 if (mucOptions.mamSupport()) {
714 this.binding.mucInfoMam.setText(R.string.server_info_available);
715 } else {
716 this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
717 }
718 if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
719 this.binding.changeConferenceButton.setVisibility(View.VISIBLE);
720 } else {
721 this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
722 }
723 } else {
724 this.binding.usersWrapper.setVisibility(View.GONE);
725 this.binding.mucInfoMore.setVisibility(View.GONE);
726 this.binding.mucSettings.setVisibility(View.GONE);
727 }
728
729 final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
730 if (mutedTill == Long.MAX_VALUE) {
731 this.binding.notificationStatusText.setText(R.string.notify_never);
732 this.binding.notificationStatusButton.setImageResource(
733 R.drawable.ic_notifications_off_24dp);
734 } else if (System.currentTimeMillis() < mutedTill) {
735 this.binding.notificationStatusText.setText(R.string.notify_paused);
736 this.binding.notificationStatusButton.setImageResource(
737 R.drawable.ic_notifications_paused_24dp);
738 } else if (mConversation.alwaysNotify()) {
739 this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
740 this.binding.notificationStatusButton.setImageResource(
741 R.drawable.ic_notifications_24dp);
742 } else if (mConversation.notifyReplies()) {
743 this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted_or_replied);
744 this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
745 } else {
746 this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
747 this.binding.notificationStatusButton.setImageResource(
748 R.drawable.ic_notifications_none_24dp);
749 }
750 final List<User> users = mucOptions.getUsers();
751 Collections.sort(
752 users,
753 (a, b) -> {
754 if (b.getAffiliation().outranks(a.getAffiliation())) {
755 return 1;
756 } else if (a.getAffiliation().outranks(b.getAffiliation())) {
757 return -1;
758 } else {
759 if (a.getAvatar() != null && b.getAvatar() == null) {
760 return -1;
761 } else if (a.getAvatar() == null && b.getAvatar() != null) {
762 return 1;
763 } else {
764 return a.getComparableName().compareToIgnoreCase(b.getComparableName());
765 }
766 }
767 });
768 this.mUserPreviewAdapter.submitList(
769 MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
770 this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
771 this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE);
772 this.binding.showUsers.setText(
773 getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
774 this.binding.usersWrapper.setVisibility(
775 users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
776 if (users.size() == 0) {
777 this.binding.noUsersHints.setText(
778 mucOptions.isPrivateAndNonAnonymous()
779 ? R.string.no_users_hint_group_chat
780 : R.string.no_users_hint_channel);
781 this.binding.noUsersHints.setVisibility(View.VISIBLE);
782 } else {
783 this.binding.noUsersHints.setVisibility(View.GONE);
784 }
785
786 if (bookmark == null) {
787 binding.tags.setVisibility(View.GONE);
788 return;
789 }
790
791 final List<Conversation.Thread> recentThreads = mConversation.recentThreads();
792 if (recentThreads.isEmpty()) {
793 this.binding.recentThreadsWrapper.setVisibility(View.GONE);
794 } else {
795 final ThreadAdapter threads = new ThreadAdapter();
796 threads.addAll(recentThreads);
797 this.binding.recentThreads.setAdapter(threads);
798 this.binding.recentThreadsWrapper.setVisibility(View.VISIBLE);
799 Util.justifyListViewHeightBasedOnChildren(binding.recentThreads);
800 }
801
802 final List<ListItem.Tag> tagList = bookmark.getTags(this);
803 if (tagList.isEmpty() || !this.showDynamicTags) {
804 binding.tags.setVisibility(View.GONE);
805 } else {
806 final LayoutInflater inflater = getLayoutInflater();
807 binding.tags.setVisibility(View.VISIBLE);
808 binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
809 final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
810 for (final ListItem.Tag tag : tagList) {
811 final String name = tag.getName();
812 final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
813 tv.setText(name);
814 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
815 final int id = ViewCompat.generateViewId();
816 tv.setId(id);
817 viewIdBuilder.add(id);
818 binding.tags.addView(tv);
819 }
820 binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
821 }
822 }
823
824 public static String getStatus(Context context, User user, final boolean advanced) {
825 if (advanced) {
826 return String.format(
827 "%s (%s)",
828 context.getString(user.getAffiliation().getResId()),
829 context.getString(user.getRole().getResId()));
830 } else {
831 return context.getString(user.getAffiliation().getResId());
832 }
833 }
834
835 private String getStatus(User user) {
836 return getStatus(this, user, mAdvancedMode);
837 }
838
839 @Override
840 public void onAffiliationChangedSuccessful(Jid jid) {
841 refreshUi();
842 }
843
844 @Override
845 public void onAffiliationChangeFailed(Jid jid, int resId) {
846 displayToast(getString(resId, jid.asBareJid().toEscapedString()));
847 }
848
849 @Override
850 public void onRoomDestroySucceeded() {
851 finish();
852 }
853
854 @Override
855 public void onRoomDestroyFailed() {
856 final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
857 displayToast(
858 getString(
859 groupChat
860 ? R.string.could_not_destroy_room
861 : R.string.could_not_destroy_channel));
862 }
863
864 @Override
865 public void onPushSucceeded() {
866 displayToast(getString(R.string.modified_conference_options));
867 }
868
869 @Override
870 public void onPushFailed() {
871 displayToast(getString(R.string.could_not_modify_conference_options));
872 }
873
874 private void displayToast(final String msg) {
875 runOnUiThread(
876 () -> {
877 if (isFinishing()) {
878 return;
879 }
880 ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
881 });
882 }
883
884 @Override
885 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
886
887 @Override
888 public void onTextChanged(CharSequence s, int start, int before, int count) {}
889
890 @Override
891 public void afterTextChanged(Editable s) {
892 if (mConversation == null) {
893 return;
894 }
895 final MucOptions mucOptions = mConversation.getMucOptions();
896 if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
897 boolean subjectChanged =
898 changed(
899 binding.mucEditSubject.getEditableText().toString(),
900 mucOptions.getSubject());
901 boolean nameChanged =
902 changed(
903 binding.mucEditTitle.getEditableText().toString(),
904 mucOptions.getName());
905 final Bookmark bookmark = mConversation.getBookmark();
906 if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) {
907 this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
908 this.binding.editMucNameButton.setContentDescription(getString(R.string.save));
909 } else {
910 this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
911 this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
912 }
913 }
914 }
915
916 class ThreadAdapter extends ArrayAdapter<Conversation.Thread> {
917 ThreadAdapter() { super(ConferenceDetailsActivity.this, 0); }
918
919 @Override
920 public View getView(int position, View view, @NonNull ViewGroup parent) {
921 final ThreadRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.thread_row, parent, false);
922 final Conversation.Thread item = getItem(position);
923
924 binding.threadIdenticon.setColor(UIHelper.getColorForName(item.getThreadId()));
925 binding.threadIdenticon.setHash(UIHelper.identiconHash(item.getThreadId()));
926
927 binding.threadSubject.setText(item.getDisplay());
928
929 return binding.getRoot();
930 }
931 }
932}