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.SpannableString;
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 de.gultsch.common.Linkify;
53import eu.siacs.conversations.R;
54import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
55import eu.siacs.conversations.databinding.ThreadRowBinding;
56import eu.siacs.conversations.entities.Account;
57import eu.siacs.conversations.entities.Bookmark;
58import eu.siacs.conversations.entities.Contact;
59import eu.siacs.conversations.entities.Conversation;
60import eu.siacs.conversations.entities.ListItem;
61import eu.siacs.conversations.entities.MucOptions;
62import eu.siacs.conversations.entities.MucOptions.User;
63import eu.siacs.conversations.services.XmppConnectionService;
64import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
65import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
66import eu.siacs.conversations.ui.adapter.MediaAdapter;
67import eu.siacs.conversations.ui.adapter.UserPreviewAdapter;
68import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
69import eu.siacs.conversations.ui.text.FixedURLSpan;
70import eu.siacs.conversations.ui.util.Attachment;
71import eu.siacs.conversations.ui.util.AvatarWorkerTask;
72import eu.siacs.conversations.ui.util.GridManager;
73import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
74import eu.siacs.conversations.ui.util.MucConfiguration;
75import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
76import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
77import eu.siacs.conversations.utils.AccountUtils;
78import eu.siacs.conversations.utils.Compatibility;
79import eu.siacs.conversations.utils.StringUtils;
80import eu.siacs.conversations.utils.StylingHelper;
81import eu.siacs.conversations.utils.UIHelper;
82import eu.siacs.conversations.utils.XmppUri;
83import eu.siacs.conversations.utils.XEP0392Helper;
84import eu.siacs.conversations.xmpp.Jid;
85import eu.siacs.conversations.xmpp.XmppConnection;
86
87import me.drakeet.support.toast.ToastCompat;
88
89public class ConferenceDetailsActivity extends XmppActivity
90 implements OnConversationUpdate,
91 OnMucRosterUpdate,
92 XmppConnectionService.OnAffiliationChanged,
93 XmppConnectionService.OnConfigurationPushed,
94 XmppConnectionService.OnRoomDestroy,
95 TextWatcher,
96 OnMediaLoaded {
97 public static final String ACTION_VIEW_MUC = "view_muc";
98
99 private Conversation mConversation;
100 private ActivityMucDetailsBinding binding;
101 private MediaAdapter mMediaAdapter;
102 private UserPreviewAdapter mUserPreviewAdapter;
103 private String uuid = null;
104
105 private boolean mAdvancedMode = false;
106 private boolean showDynamicTags = true;
107
108 private final UiCallback<Conversation> renameCallback =
109 new UiCallback<Conversation>() {
110 @Override
111 public void success(Conversation object) {
112 displayToast(getString(R.string.your_nick_has_been_changed));
113 runOnUiThread(
114 () -> {
115 updateView();
116 });
117 }
118
119 @Override
120 public void error(final int errorCode, Conversation object) {
121 displayToast(getString(errorCode));
122 }
123
124 @Override
125 public void userInputRequired(PendingIntent pi, Conversation object) {}
126 };
127
128 public static void open(final Activity activity, final Conversation conversation) {
129 Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
130 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
131 intent.putExtra("uuid", conversation.getUuid());
132 activity.startActivity(intent);
133 }
134
135 private final OnClickListener mNotifyStatusClickListener =
136 new OnClickListener() {
137 @Override
138 public void onClick(View v) {
139 final MaterialAlertDialogBuilder builder =
140 new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
141 builder.setTitle(R.string.pref_notification_settings);
142 String[] choices = {
143 getString(R.string.notify_on_all_messages),
144 getString(R.string.notify_only_when_highlighted),
145 getString(R.string.notify_only_when_highlighted_or_replied),
146 getString(R.string.notify_never)
147 };
148 final AtomicInteger choice;
149 if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0)
150 == Long.MAX_VALUE) {
151 choice = new AtomicInteger(3);
152 } else {
153 choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1));
154 }
155 builder.setSingleChoiceItems(
156 choices, choice.get(), (dialog, which) -> choice.set(which));
157 builder.setNegativeButton(R.string.cancel, null);
158 builder.setPositiveButton(
159 R.string.ok,
160 (dialog, which) -> {
161 if (choice.get() == 3) {
162 mConversation.setMutedTill(Long.MAX_VALUE);
163 } else {
164 mConversation.setMutedTill(0);
165 mConversation.setAttribute(
166 Conversation.ATTRIBUTE_ALWAYS_NOTIFY,
167 String.valueOf(choice.get() == 0));
168 mConversation.setAttribute(
169 Conversation.ATTRIBUTE_NOTIFY_REPLIES,
170 String.valueOf(choice.get() == 2));
171 }
172 xmppConnectionService.updateConversation(mConversation);
173 updateView();
174 });
175 builder.create().show();
176 }
177 };
178
179 private final OnClickListener mChangeConferenceSettings =
180 new OnClickListener() {
181 @Override
182 public void onClick(View v) {
183 final MucOptions mucOptions = mConversation.getMucOptions();
184 final MaterialAlertDialogBuilder builder =
185 new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
186 MucConfiguration configuration =
187 MucConfiguration.get(
188 ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
189 builder.setTitle(configuration.title);
190 final boolean[] values = configuration.values;
191 builder.setMultiChoiceItems(
192 configuration.names,
193 values,
194 (dialog, which, isChecked) -> values[which] = isChecked);
195 builder.setNegativeButton(R.string.cancel, null);
196 builder.setPositiveButton(
197 R.string.confirm,
198 (dialog, which) -> {
199 final Bundle options = configuration.toBundle(values);
200 options.putString("muc#roomconfig_persistentroom", "1");
201 if (options.containsKey("muc#roomconfig_allowinvites")) {
202 options.putString(
203 "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites",
204 options.getString("muc#roomconfig_allowinvites"));
205 }
206 xmppConnectionService.pushConferenceConfiguration(
207 mConversation, options, ConferenceDetailsActivity.this);
208 });
209 builder.create().show();
210 }
211 };
212
213 @Override
214 public void onConversationUpdate() {
215 refreshUi();
216 }
217
218 @Override
219 public void onMucRosterUpdate() {
220 refreshUi();
221 }
222
223 @Override
224 protected void refreshUiReal() {
225 updateView();
226 }
227
228 @Override
229 protected void onCreate(Bundle savedInstanceState) {
230 super.onCreate(savedInstanceState);
231 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details);
232 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
233 showDynamicTags = preferences.getBoolean("show_dynamic_tags", getResources().getBoolean(R.bool.show_dynamic_tags));
234 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
235 this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
236 setSupportActionBar(binding.toolbar);
237 configureActionBar(getSupportActionBar());
238 this.binding.editNickButton.setOnClickListener(
239 v ->
240 quickEdit(
241 mConversation.getMucOptions().getActualNick(),
242 R.string.nickname,
243 value -> {
244 if (xmppConnectionService.renameInMuc(
245 mConversation, value, renameCallback)) {
246 return null;
247 } else {
248 return getString(R.string.invalid_muc_nick);
249 }
250 }));
251 this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
252 this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
253 this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
254 this.binding.yourPhoto.setOnClickListener(
255 v -> {
256 final MucOptions mucOptions = mConversation.getMucOptions();
257 if (!mucOptions.hasVCards()) {
258 Toast.makeText(
259 this,
260 R.string.host_does_not_support_group_chat_avatars,
261 Toast.LENGTH_SHORT)
262 .show();
263 return;
264 }
265 if (!mucOptions
266 .getSelf()
267 .getAffiliation()
268 .ranks(MucOptions.Affiliation.OWNER)) {
269 Toast.makeText(
270 this,
271 R.string.only_the_owner_can_change_group_chat_avatar,
272 Toast.LENGTH_SHORT)
273 .show();
274 return;
275 }
276 final Intent intent =
277 new Intent(this, PublishGroupChatProfilePictureActivity.class);
278 intent.putExtra("uuid", mConversation.getUuid());
279 startActivity(intent);
280 });
281 this.binding.yourPhoto.setOnLongClickListener(v -> {
282 PopupMenu popupMenu = new PopupMenu(this, v);
283 popupMenu.inflate(R.menu.conference_photo);
284 popupMenu.setOnMenuItemClickListener(menuItem -> {
285 switch (menuItem.getItemId()) {
286 case R.id.action_block_avatar:
287 new AlertDialog.Builder(this)
288 .setTitle(R.string.block_media)
289 .setMessage("Do you really want to block this avatar?")
290 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
291 xmppConnectionService.blockMedia(xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatarFilename()));
292 xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatarFilename()).delete();
293 avatarService().clear(mConversation);
294 mConversation.getContact().setAvatar(null);
295 xmppConnectionService.updateConversationUi();
296 })
297 .setNegativeButton(R.string.no, null).show();
298 return true;
299 }
300 return true;
301 });
302 popupMenu.show();
303 return true;
304 });
305 this.binding.editMucNameButton.setContentDescription(
306 getString(R.string.edit_name_and_topic));
307 this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
308 this.binding.mucEditTitle.addTextChangedListener(this);
309 this.binding.mucEditSubject.addTextChangedListener(this);
310 //this.binding.mucEditSubject.addTextChangedListener(
311 // new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
312 this.binding.editTags.addTextChangedListener(this);
313 this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
314 this.mUserPreviewAdapter = new UserPreviewAdapter();
315 this.binding.media.setAdapter(mMediaAdapter);
316 this.binding.users.setAdapter(mUserPreviewAdapter);
317 GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
318 GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size);
319 this.binding.recentThreads.setOnItemClickListener((a0, v, pos, a3) -> {
320 final Conversation.Thread thread = (Conversation.Thread) binding.recentThreads.getAdapter().getItem(pos);
321 switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId());
322 });
323 this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
324 this.binding.showUsers.setOnClickListener(
325 v -> {
326 Intent intent = new Intent(this, MucUsersActivity.class);
327 intent.putExtra("uuid", mConversation.getUuid());
328 startActivity(intent);
329 });
330 this.binding.relatedMucs.setOnClickListener(v -> {
331 final Intent intent = new Intent(this, ChannelDiscoveryActivity.class);
332 intent.putExtra("services", new String[]{ mConversation.getJid().getDomain().toString(), mConversation.getAccount().getJid().toString() });
333 startActivity(intent);
334 });
335 }
336
337 @Override
338 public void onStart() {
339 super.onStart();
340 binding.mediaWrapper.setVisibility(
341 Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
342 }
343
344 @Override
345 public boolean onOptionsItemSelected(MenuItem menuItem) {
346 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
347 return false;
348 }
349 switch (menuItem.getItemId()) {
350 case android.R.id.home:
351 finish();
352 break;
353 case R.id.action_share_http:
354 shareLink(true);
355 break;
356 case R.id.action_share_uri:
357 shareLink(false);
358 break;
359 case R.id.action_save_as_bookmark:
360 saveAsBookmark();
361 break;
362 case R.id.action_destroy_room:
363 destroyRoom();
364 break;
365 case R.id.action_advanced_mode:
366 this.mAdvancedMode = !menuItem.isChecked();
367 menuItem.setChecked(this.mAdvancedMode);
368 getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
369 final boolean online =
370 mConversation != null && mConversation.getMucOptions().online();
371 this.binding.mucInfoMore.setVisibility(
372 this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
373 invalidateOptionsMenu();
374 updateView();
375 break;
376 case R.id.action_custom_notifications:
377 if (mConversation != null) {
378 configureCustomNotifications(mConversation);
379 }
380 break;
381 }
382 return super.onOptionsItemSelected(menuItem);
383 }
384
385 private void configureCustomNotifications(final Conversation conversation) {
386 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
387 || conversation.getMode() != Conversation.MODE_MULTI) {
388 return;
389 }
390 final var shortcut =
391 xmppConnectionService
392 .getShortcutService()
393 .getShortcutInfo(conversation.getMucOptions());
394 configureCustomNotification(shortcut);
395 }
396
397 @Override
398 public boolean onContextItemSelected(@NonNull final MenuItem item) {
399 final User user = mUserPreviewAdapter.getSelectedUser();
400 if (user == null) {
401 Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show();
402 return true;
403 }
404 if (!MucDetailsContextMenuHelper.onContextItemSelected(
405 item, mUserPreviewAdapter.getSelectedUser(), this)) {
406 return super.onContextItemSelected(item);
407 }
408 return true;
409 }
410
411 public void onMucEditButtonClicked(View v) {
412 if (this.binding.mucEditor.getVisibility() == View.GONE) {
413 final MucOptions mucOptions = mConversation.getMucOptions();
414 this.binding.mucEditor.setVisibility(View.VISIBLE);
415 this.binding.mucDisplay.setVisibility(View.GONE);
416 this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
417 this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
418 final String name = mucOptions.getName();
419 this.binding.mucEditTitle.setText("");
420 final boolean owner =
421 mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
422 if (owner || printableValue(name)) {
423 this.binding.mucEditTitle.setVisibility(View.VISIBLE);
424 if (name != null) {
425 this.binding.mucEditTitle.append(name);
426 }
427 } else {
428 this.binding.mucEditTitle.setVisibility(View.GONE);
429 }
430 this.binding.mucEditTitle.setEnabled(owner);
431 final String subject = mucOptions.getSubject();
432 this.binding.mucEditSubject.setText("");
433 if (subject != null) {
434 this.binding.mucEditSubject.append(subject);
435 }
436 this.binding.mucEditSubject.setEnabled(mucOptions.canChangeSubject());
437 if (!owner) {
438 this.binding.mucEditSubject.requestFocus();
439 }
440
441 final Bookmark bookmark = mConversation.getBookmark();
442 if (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2() && showDynamicTags) {
443 for (final ListItem.Tag group : bookmark.getGroupTags()) {
444 binding.editTags.addObjectSync(group);
445 }
446 ArrayList<ListItem.Tag> tags = new ArrayList<>();
447 for (final Account account : xmppConnectionService.getAccounts()) {
448 for (Contact contact : account.getRoster().getContacts()) {
449 tags.addAll(contact.getTags(this));
450 }
451 for (Bookmark bmark : account.getBookmarks()) {
452 tags.addAll(bmark.getTags(this));
453 }
454 }
455 Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
456 sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
457
458 ArrayAdapter<ListItem.Tag> adapter = new ArrayAdapter<>(
459 this,
460 android.R.layout.simple_list_item_1,
461 tags.stream()
462 .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
463 .entrySet().stream()
464 .sorted(sortTagsBy)
465 .map(e -> e.getKey()).collect(Collectors.toList())
466 );
467 binding.editTags.setAdapter(adapter);
468 this.binding.editTags.setVisibility(View.VISIBLE);
469 } else {
470 this.binding.editTags.setVisibility(View.GONE);
471 }
472 } else {
473 String subject =
474 this.binding.mucEditSubject.isEnabled()
475 ? this.binding.mucEditSubject.getEditableText().toString().trim()
476 : null;
477 String name =
478 this.binding.mucEditTitle.isEnabled()
479 ? this.binding.mucEditTitle.getEditableText().toString().trim()
480 : null;
481 onMucInfoUpdated(subject, name);
482
483 final Bookmark bookmark = mConversation.getBookmark();
484 if (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2()) {
485 bookmark.setGroups(binding.editTags.getObjects().stream().map(tag -> tag.getName()).collect(Collectors.toList()));
486 xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
487 }
488
489 SoftKeyboardUtils.hideSoftKeyboard(this);
490 hideEditor();
491 updateView();
492 }
493 }
494
495 private void hideEditor() {
496 this.binding.mucEditor.setVisibility(View.GONE);
497 this.binding.mucDisplay.setVisibility(View.VISIBLE);
498 this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
499 this.binding.editMucNameButton.setContentDescription(
500 getString(R.string.edit_name_and_topic));
501 }
502
503 private void onMucInfoUpdated(String subject, String name) {
504 final MucOptions mucOptions = mConversation.getMucOptions();
505 if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
506 xmppConnectionService.pushSubjectToConference(mConversation, subject);
507 }
508 if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)
509 && changed(mucOptions.getName(), name)) {
510 Bundle options = new Bundle();
511 options.putString("muc#roomconfig_persistentroom", "1");
512 options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name));
513 xmppConnectionService.pushConferenceConfiguration(mConversation, options, this);
514 }
515 }
516
517 @Override
518 protected String getShareableUri(boolean http) {
519 if (mConversation != null) {
520 if (http) {
521 return "https://conversations.im/j/"
522 + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toString());
523 } else {
524 return "xmpp:" + Uri.encode(mConversation.getJid().asBareJid().toString(), "@/+") + "?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().toString();
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().toString());
659 if (mAdvancedMode) this.binding.truejid.setVisibility(View.VISIBLE);
660 } else {
661 this.binding.jid.setText(mConversation.getJid().asBareJid().toString());
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 final var spannable = new SpannableString(subject);
682 StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
683 Linkify.addLinks(spannable);
684 FixedURLSpan.fix(spannable);
685 this.binding.mucSubject.setText(spannable);
686 this.binding.mucSubject.setTextAppearance(
687 subject.length() > (hasTitle ? 128 : 196)
688 ? com.google.android.material.R.style
689 .TextAppearance_Material3_BodyMedium
690 : com.google.android.material.R.style
691 .TextAppearance_Material3_BodyLarge);
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().toString()));
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}