ConferenceDetailsActivity.java

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