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.os.Build;
 10import android.os.Bundle;
 11import android.text.Editable;
 12import android.text.SpannableString;
 13import android.text.TextWatcher;
 14import android.text.method.LinkMovementMethod;
 15import android.view.Menu;
 16import android.view.MenuItem;
 17import android.view.View;
 18import android.view.View.OnClickListener;
 19import android.widget.Toast;
 20import androidx.annotation.NonNull;
 21import androidx.annotation.StringRes;
 22import androidx.appcompat.app.AlertDialog;
 23import androidx.core.content.ContextCompat;
 24import androidx.databinding.DataBindingUtil;
 25import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 26import com.google.common.base.Strings;
 27import com.google.common.collect.ImmutableMap;
 28import com.google.common.util.concurrent.FutureCallback;
 29import com.google.common.util.concurrent.Futures;
 30import de.gultsch.common.Linkify;
 31import eu.siacs.conversations.R;
 32import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
 33import eu.siacs.conversations.entities.Conversation;
 34import eu.siacs.conversations.entities.Conversational;
 35import eu.siacs.conversations.entities.MucOptions;
 36import eu.siacs.conversations.entities.MucOptions.User;
 37import eu.siacs.conversations.services.XmppConnectionService;
 38import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
 39import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
 40import eu.siacs.conversations.ui.adapter.MediaAdapter;
 41import eu.siacs.conversations.ui.adapter.UserPreviewAdapter;
 42import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 43import eu.siacs.conversations.ui.text.FixedURLSpan;
 44import eu.siacs.conversations.ui.util.Attachment;
 45import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 46import eu.siacs.conversations.ui.util.GridManager;
 47import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 48import eu.siacs.conversations.ui.util.MucConfiguration;
 49import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
 50import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 51import eu.siacs.conversations.utils.AccountUtils;
 52import eu.siacs.conversations.utils.Compatibility;
 53import eu.siacs.conversations.utils.StylingHelper;
 54import eu.siacs.conversations.utils.XmppUri;
 55import eu.siacs.conversations.xmpp.Jid;
 56import eu.siacs.conversations.xmpp.manager.BookmarkManager;
 57import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
 58import im.conversations.android.xmpp.model.muc.Affiliation;
 59import im.conversations.android.xmpp.model.muc.Role;
 60import java.util.Collections;
 61import java.util.List;
 62import java.util.concurrent.atomic.AtomicInteger;
 63import me.drakeet.support.toast.ToastCompat;
 64
 65public class ConferenceDetailsActivity extends XmppActivity
 66        implements OnConversationUpdate,
 67                OnMucRosterUpdate,
 68                XmppConnectionService.OnAffiliationChanged,
 69                TextWatcher,
 70                OnMediaLoaded {
 71    public static final String ACTION_VIEW_MUC = "view_muc";
 72
 73    private Conversation mConversation;
 74    private ActivityMucDetailsBinding binding;
 75    private MediaAdapter mMediaAdapter;
 76    private UserPreviewAdapter mUserPreviewAdapter;
 77    private String uuid = null;
 78
 79    private boolean mAdvancedMode = false;
 80
 81    private FutureCallback<Void> renameCallback =
 82            new FutureCallback<Void>() {
 83                @Override
 84                public void onSuccess(Void result) {
 85                    displayToast(getString(R.string.your_nick_has_been_changed));
 86                    updateView();
 87                }
 88
 89                @Override
 90                public void onFailure(Throwable t) {
 91
 92                    // TODO check for NickInUseException and NickInvalid exception
 93
 94                }
 95            };
 96
 97    public static void open(final Activity activity, final Conversation conversation) {
 98        Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
 99        intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
100        intent.putExtra("uuid", conversation.getUuid());
101        activity.startActivity(intent);
102    }
103
104    private final OnClickListener mNotifyStatusClickListener =
105            new OnClickListener() {
106                @Override
107                public void onClick(View v) {
108                    final MaterialAlertDialogBuilder builder =
109                            new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
110                    builder.setTitle(R.string.pref_notification_settings);
111                    String[] choices = {
112                        getString(R.string.notify_on_all_messages),
113                        getString(R.string.notify_only_when_highlighted),
114                        getString(R.string.notify_never)
115                    };
116                    final AtomicInteger choice;
117                    if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0)
118                            == Long.MAX_VALUE) {
119                        choice = new AtomicInteger(2);
120                    } else {
121                        choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : 1);
122                    }
123                    builder.setSingleChoiceItems(
124                            choices, choice.get(), (dialog, which) -> choice.set(which));
125                    builder.setNegativeButton(R.string.cancel, null);
126                    builder.setPositiveButton(
127                            R.string.ok,
128                            (dialog, which) -> {
129                                if (choice.get() == 2) {
130                                    mConversation.setMutedTill(Long.MAX_VALUE);
131                                } else {
132                                    mConversation.setMutedTill(0);
133                                    mConversation.setAttribute(
134                                            Conversation.ATTRIBUTE_ALWAYS_NOTIFY,
135                                            String.valueOf(choice.get() == 0));
136                                }
137                                xmppConnectionService.updateConversation(mConversation);
138                                updateView();
139                            });
140                    builder.create().show();
141                }
142            };
143
144    private final FutureCallback<Void> onConfigurationPushed =
145            new FutureCallback<Void>() {
146
147                @Override
148                public void onSuccess(Void result) {
149                    displayToast(getString(R.string.modified_conference_options));
150                }
151
152                @Override
153                public void onFailure(Throwable t) {
154                    displayToast(getString(R.string.could_not_modify_conference_options));
155                }
156            };
157
158    private final OnClickListener mChangeConferenceSettings =
159            new OnClickListener() {
160                @Override
161                public void onClick(View v) {
162                    final MucOptions mucOptions = mConversation.getMucOptions();
163                    final MaterialAlertDialogBuilder builder =
164                            new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
165                    MucConfiguration configuration =
166                            MucConfiguration.get(
167                                    ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
168                    builder.setTitle(configuration.title);
169                    final boolean[] values = configuration.values;
170                    builder.setMultiChoiceItems(
171                            configuration.names,
172                            values,
173                            (dialog, which, isChecked) -> values[which] = isChecked);
174                    builder.setNegativeButton(R.string.cancel, null);
175                    builder.setPositiveButton(
176                            R.string.confirm,
177                            (dialog, which) -> {
178                                final var options = configuration.toBundle(values);
179                                final var future =
180                                        mConversation
181                                                .getAccount()
182                                                .getXmppConnection()
183                                                .getManager(MultiUserChatManager.class)
184                                                .pushConfiguration(mConversation, options);
185                                Futures.addCallback(
186                                        future,
187                                        onConfigurationPushed,
188                                        ContextCompat.getMainExecutor(getApplication()));
189                            });
190                    builder.create().show();
191                }
192            };
193
194    @Override
195    public void onConversationUpdate() {
196        refreshUi();
197    }
198
199    @Override
200    public void onMucRosterUpdate() {
201        refreshUi();
202    }
203
204    @Override
205    protected void refreshUiReal() {
206        updateView();
207    }
208
209    @Override
210    protected void onCreate(Bundle savedInstanceState) {
211        super.onCreate(savedInstanceState);
212        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details);
213        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
214        this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
215        setSupportActionBar(binding.toolbar);
216        configureActionBar(getSupportActionBar());
217        this.binding.editNickButton.setOnClickListener(
218                v ->
219                        quickEdit(
220                                mConversation.getMucOptions().getActualNick(),
221                                R.string.nickname,
222                                value -> {
223                                    if (mConversation.getMucOptions().createJoinJid(value)
224                                            == null) {
225                                        return getString(R.string.invalid_muc_nick);
226                                    }
227                                    final var future =
228                                            mConversation
229                                                    .getAccount()
230                                                    .getXmppConnection()
231                                                    .getManager(MultiUserChatManager.class)
232                                                    .changeUsername(mConversation, value);
233                                    Futures.addCallback(
234                                            future,
235                                            renameCallback,
236                                            ContextCompat.getMainExecutor(this));
237                                    return null;
238                                }));
239        this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
240        this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
241        this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
242        this.binding.yourPhoto.setOnClickListener(
243                v -> {
244                    final MucOptions mucOptions = mConversation.getMucOptions();
245                    if (!mucOptions.hasVCards()) {
246                        Toast.makeText(
247                                        this,
248                                        R.string.host_does_not_support_group_chat_avatars,
249                                        Toast.LENGTH_SHORT)
250                                .show();
251                        return;
252                    }
253                    if (!mucOptions.getSelf().ranks(Affiliation.OWNER)) {
254                        Toast.makeText(
255                                        this,
256                                        R.string.only_the_owner_can_change_group_chat_avatar,
257                                        Toast.LENGTH_SHORT)
258                                .show();
259                        return;
260                    }
261                    final Intent intent =
262                            new Intent(this, PublishGroupChatProfilePictureActivity.class);
263                    intent.putExtra("uuid", mConversation.getUuid());
264                    startActivity(intent);
265                });
266        this.binding.editMucNameButton.setContentDescription(
267                getString(R.string.edit_name_and_topic));
268        this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
269        this.binding.mucEditTitle.addTextChangedListener(this);
270        this.binding.mucEditSubject.addTextChangedListener(this);
271        this.binding.mucEditSubject.addTextChangedListener(
272                new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
273        this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
274        this.mUserPreviewAdapter = new UserPreviewAdapter();
275        this.binding.media.setAdapter(mMediaAdapter);
276        this.binding.users.setAdapter(mUserPreviewAdapter);
277        GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
278        GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size);
279        this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
280        this.binding.showUsers.setOnClickListener(
281                v -> {
282                    Intent intent = new Intent(this, MucUsersActivity.class);
283                    intent.putExtra("uuid", mConversation.getUuid());
284                    startActivity(intent);
285                });
286    }
287
288    @Override
289    public void onStart() {
290        super.onStart();
291        binding.mediaWrapper.setVisibility(
292                Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
293    }
294
295    @Override
296    public boolean onOptionsItemSelected(MenuItem menuItem) {
297        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
298            return false;
299        }
300        switch (menuItem.getItemId()) {
301            case android.R.id.home:
302                finish();
303                break;
304            case R.id.action_share_http:
305                shareLink(true);
306                break;
307            case R.id.action_share_uri:
308                shareLink(false);
309                break;
310            case R.id.action_save_as_bookmark:
311                saveAsBookmark();
312                break;
313            case R.id.action_destroy_room:
314                destroyRoom();
315                break;
316            case R.id.action_advanced_mode:
317                this.mAdvancedMode = !menuItem.isChecked();
318                menuItem.setChecked(this.mAdvancedMode);
319                getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
320                final boolean online =
321                        mConversation != null && mConversation.getMucOptions().online();
322                this.binding.mucInfoMore.setVisibility(
323                        this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
324                invalidateOptionsMenu();
325                updateView();
326                break;
327            case R.id.action_custom_notifications:
328                if (mConversation != null) {
329                    configureCustomNotifications(mConversation);
330                }
331                break;
332        }
333        return super.onOptionsItemSelected(menuItem);
334    }
335
336    private void configureCustomNotifications(final Conversation conversation) {
337        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
338                || conversation.getMode() != Conversational.MODE_MULTI) {
339            return;
340        }
341        final var shortcut =
342                xmppConnectionService
343                        .getShortcutService()
344                        .getShortcutInfo(conversation.getMucOptions());
345        configureCustomNotification(shortcut);
346    }
347
348    @Override
349    public boolean onContextItemSelected(@NonNull final MenuItem item) {
350        final User user = mUserPreviewAdapter.getSelectedUser();
351        if (user == null) {
352            Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show();
353            return true;
354        }
355        if (!MucDetailsContextMenuHelper.onContextItemSelected(
356                item, mUserPreviewAdapter.getSelectedUser(), this)) {
357            return super.onContextItemSelected(item);
358        }
359        return true;
360    }
361
362    public void onMucEditButtonClicked(View v) {
363        if (this.binding.mucEditor.getVisibility() == View.GONE) {
364            final MucOptions mucOptions = mConversation.getMucOptions();
365            this.binding.mucEditor.setVisibility(View.VISIBLE);
366            this.binding.mucDisplay.setVisibility(View.GONE);
367            this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
368            this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
369            final String name = mucOptions.getName();
370            this.binding.mucEditTitle.setText("");
371            final boolean owner = mucOptions.getSelf().ranks(Affiliation.OWNER);
372            if (owner || printableValue(name)) {
373                this.binding.mucEditTitle.setVisibility(View.VISIBLE);
374                if (name != null) {
375                    this.binding.mucEditTitle.append(name);
376                }
377            } else {
378                this.binding.mucEditTitle.setVisibility(View.GONE);
379            }
380            this.binding.mucEditTitle.setEnabled(owner);
381            final String subject = mucOptions.getSubject();
382            this.binding.mucEditSubject.setText("");
383            if (subject != null) {
384                this.binding.mucEditSubject.append(subject);
385            }
386            this.binding.mucEditSubject.setEnabled(mucOptions.canChangeSubject());
387            if (!owner) {
388                this.binding.mucEditSubject.requestFocus();
389            }
390        } else {
391            String subject =
392                    this.binding.mucEditSubject.isEnabled()
393                            ? this.binding.mucEditSubject.getEditableText().toString().trim()
394                            : null;
395            String name =
396                    this.binding.mucEditTitle.isEnabled()
397                            ? this.binding.mucEditTitle.getEditableText().toString().trim()
398                            : null;
399            onMucInfoUpdated(subject, name);
400            SoftKeyboardUtils.hideSoftKeyboard(this);
401            hideEditor();
402        }
403    }
404
405    private void hideEditor() {
406        this.binding.mucEditor.setVisibility(View.GONE);
407        this.binding.mucDisplay.setVisibility(View.VISIBLE);
408        this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
409        this.binding.editMucNameButton.setContentDescription(
410                getString(R.string.edit_name_and_topic));
411    }
412
413    private void onMucInfoUpdated(String subject, String name) {
414        final var account = mConversation.getAccount();
415        final MucOptions mucOptions = mConversation.getMucOptions();
416        if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
417            xmppConnectionService.pushSubjectToConference(mConversation, subject);
418        }
419        if (mucOptions.getSelf().ranks(Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
420            final var options =
421                    new ImmutableMap.Builder<String, Object>()
422                            .put("muc#roomconfig_persistentroom", true)
423                            .put("muc#roomconfig_roomname", Strings.nullToEmpty(name))
424                            .build();
425            final var future =
426                    account.getXmppConnection()
427                            .getManager(MultiUserChatManager.class)
428                            .pushConfiguration(mConversation, options);
429            Futures.addCallback(
430                    future, onConfigurationPushed, ContextCompat.getMainExecutor(getApplication()));
431        }
432    }
433
434    @Override
435    protected String getShareableUri(boolean http) {
436        if (mConversation != null) {
437            if (http) {
438                return "https://conversations.im/j/"
439                        + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toString());
440            } else {
441                return "xmpp:" + mConversation.getJid().asBareJid() + "?join";
442            }
443        } else {
444            return null;
445        }
446    }
447
448    @Override
449    public boolean onPrepareOptionsMenu(final Menu menu) {
450        final MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark);
451        final MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode);
452        final MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room);
453        menuItemAdvancedMode.setChecked(mAdvancedMode);
454        if (mConversation == null) {
455            return true;
456        }
457        menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
458        menuItemDestroyRoom.setVisible(
459                mConversation.getMucOptions().getSelf().ranks(Affiliation.OWNER));
460        return true;
461    }
462
463    @Override
464    public boolean onCreateOptionsMenu(final Menu menu) {
465        final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
466        getMenuInflater().inflate(R.menu.muc_details, menu);
467        final MenuItem share = menu.findItem(R.id.action_share);
468        share.setVisible(!groupChat);
469        final MenuItem destroy = menu.findItem(R.id.action_destroy_room);
470        destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
471        AccountUtils.showHideMenuItems(menu);
472        final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
473        customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
474        return super.onCreateOptionsMenu(menu);
475    }
476
477    @Override
478    public void onMediaLoaded(final List<Attachment> attachments) {
479        runOnUiThread(
480                () -> {
481                    final int limit = GridManager.getCurrentColumnCount(binding.media);
482                    mMediaAdapter.setAttachments(
483                            attachments.subList(0, Math.min(limit, attachments.size())));
484                    binding.mediaWrapper.setVisibility(
485                            attachments.isEmpty() ? View.GONE : View.VISIBLE);
486                });
487    }
488
489    protected void saveAsBookmark() {
490        final var account = mConversation.getAccount();
491        account.getXmppConnection()
492                .getManager(BookmarkManager.class)
493                .save(mConversation, mConversation.getMucOptions().getName());
494    }
495
496    protected void destroyRoom() {
497        final var destroyCallBack =
498                new FutureCallback<Void>() {
499
500                    @Override
501                    public void onSuccess(Void result) {
502                        finish();
503                    }
504
505                    @Override
506                    public void onFailure(Throwable t) {
507                        final boolean groupChat =
508                                mConversation != null && mConversation.isPrivateAndNonAnonymous();
509                        // TODO show toast directly
510                        displayToast(
511                                getString(
512                                        groupChat
513                                                ? R.string.could_not_destroy_room
514                                                : R.string.could_not_destroy_channel));
515                    }
516                };
517        final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
518        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
519        builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
520        builder.setMessage(
521                groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
522        builder.setPositiveButton(
523                R.string.ok,
524                (dialog, which) -> {
525                    final var future = xmppConnectionService.destroyRoom(mConversation);
526                    Futures.addCallback(
527                            future,
528                            destroyCallBack,
529                            ContextCompat.getMainExecutor(getApplication()));
530                });
531        builder.setNegativeButton(R.string.cancel, null);
532        final AlertDialog dialog = builder.create();
533        dialog.setCanceledOnTouchOutside(false);
534        dialog.show();
535    }
536
537    @Override
538    protected void onBackendConnected() {
539        if (mPendingConferenceInvite != null) {
540            mPendingConferenceInvite.execute(this);
541            mPendingConferenceInvite = null;
542        }
543        if (getIntent().getAction().equals(ACTION_VIEW_MUC)) {
544            this.uuid = getIntent().getExtras().getString("uuid");
545        }
546        if (uuid != null) {
547            this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
548            if (this.mConversation != null) {
549                if (Compatibility.hasStoragePermission(this)) {
550                    final int limit = GridManager.getCurrentColumnCount(this.binding.media);
551                    xmppConnectionService.getAttachments(this.mConversation, limit, this);
552                    this.binding.showMedia.setOnClickListener(
553                            (v) -> MediaBrowserActivity.launch(this, mConversation));
554                }
555                updateView();
556            }
557        }
558    }
559
560    @Override
561    public void onBackPressed() {
562        if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
563            hideEditor();
564        } else {
565            super.onBackPressed();
566        }
567    }
568
569    private void updateView() {
570        invalidateOptionsMenu();
571        if (mConversation == null) {
572            return;
573        }
574        final MucOptions mucOptions = mConversation.getMucOptions();
575        final User self = mucOptions.getSelf();
576        final String account = mConversation.getAccount().getJid().asBareJid().toString();
577        setTitle(
578                mucOptions.isPrivateAndNonAnonymous()
579                        ? R.string.action_muc_details
580                        : R.string.channel_details);
581        this.binding.editMucNameButton.setVisibility(
582                (self.ranks(Affiliation.OWNER) || mucOptions.canChangeSubject())
583                        ? View.VISIBLE
584                        : View.GONE);
585        this.binding.detailsAccount.setText(getString(R.string.using_account, account));
586        if (mConversation.isPrivateAndNonAnonymous()) {
587            this.binding.jid.setText(
588                    getString(R.string.hosted_on, mConversation.getJid().getDomain()));
589        } else {
590            this.binding.jid.setText(mConversation.getJid().asBareJid().toString());
591        }
592        AvatarWorkerTask.loadAvatar(
593                mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
594        String roomName = mucOptions.getName();
595        String subject = mucOptions.getSubject();
596        final boolean hasTitle;
597        if (printableValue(roomName)) {
598            this.binding.mucTitle.setText(roomName);
599            this.binding.mucTitle.setVisibility(View.VISIBLE);
600            hasTitle = true;
601        } else if (!printableValue(subject)) {
602            this.binding.mucTitle.setText(mConversation.getName());
603            hasTitle = true;
604            this.binding.mucTitle.setVisibility(View.VISIBLE);
605        } else {
606            hasTitle = false;
607            this.binding.mucTitle.setVisibility(View.GONE);
608        }
609        if (printableValue(subject)) {
610            final var spannable = new SpannableString(subject);
611            StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
612            Linkify.addLinks(spannable);
613            FixedURLSpan.fix(spannable);
614            this.binding.mucSubject.setText(spannable);
615            this.binding.mucSubject.setTextAppearance(
616                    subject.length() > (hasTitle ? 128 : 196)
617                            ? com.google.android.material.R.style
618                                    .TextAppearance_Material3_BodyMedium
619                            : com.google.android.material.R.style
620                                    .TextAppearance_Material3_BodyLarge);
621            this.binding.mucSubject.setVisibility(View.VISIBLE);
622            this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
623        } else {
624            this.binding.mucSubject.setVisibility(View.GONE);
625        }
626        this.binding.mucYourNick.setText(mucOptions.getActualNick());
627        if (mucOptions.online()) {
628            this.binding.usersWrapper.setVisibility(View.VISIBLE);
629            this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
630            this.binding.mucRole.setVisibility(View.VISIBLE);
631            this.binding.mucRole.setText(getStatus(self));
632            if (mucOptions.getSelf().ranks(Affiliation.OWNER)) {
633                this.binding.mucSettings.setVisibility(View.VISIBLE);
634                this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
635            } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
636                this.binding.mucSettings.setVisibility(View.VISIBLE);
637                this.binding.mucConferenceType.setText(
638                        R.string.group_chat_will_make_your_jabber_id_public);
639            } else {
640                this.binding.mucSettings.setVisibility(View.GONE);
641            }
642            if (mucOptions.mamSupport()) {
643                this.binding.mucInfoMam.setText(R.string.server_info_available);
644            } else {
645                this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
646            }
647            if (self.ranks(Affiliation.OWNER)) {
648                this.binding.changeConferenceButton.setVisibility(View.VISIBLE);
649            } else {
650                this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
651            }
652        } else {
653            this.binding.usersWrapper.setVisibility(View.GONE);
654            this.binding.mucInfoMore.setVisibility(View.GONE);
655            this.binding.mucSettings.setVisibility(View.GONE);
656        }
657
658        final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
659        if (mutedTill == Long.MAX_VALUE) {
660            this.binding.notificationStatusText.setText(R.string.notify_never);
661            this.binding.notificationStatusButton.setImageResource(
662                    R.drawable.ic_notifications_off_24dp);
663        } else if (System.currentTimeMillis() < mutedTill) {
664            this.binding.notificationStatusText.setText(R.string.notify_paused);
665            this.binding.notificationStatusButton.setImageResource(
666                    R.drawable.ic_notifications_paused_24dp);
667        } else if (mConversation.alwaysNotify()) {
668            this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
669            this.binding.notificationStatusButton.setImageResource(
670                    R.drawable.ic_notifications_24dp);
671        } else {
672            this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
673            this.binding.notificationStatusButton.setImageResource(
674                    R.drawable.ic_notifications_none_24dp);
675        }
676        final List<User> users = mucOptions.getUsers();
677        Collections.sort(
678                users,
679                (a, b) -> {
680                    if (b.outranks(a.getAffiliation())) {
681                        return 1;
682                    } else if (a.outranks(b.getAffiliation())) {
683                        return -1;
684                    } else {
685                        if (a.getAvatar() != null && b.getAvatar() == null) {
686                            return -1;
687                        } else if (a.getAvatar() == null && b.getAvatar() != null) {
688                            return 1;
689                        } else {
690                            return a.getComparableName().compareToIgnoreCase(b.getComparableName());
691                        }
692                    }
693                });
694        this.binding.users.post(
695                () -> {
696                    final var list =
697                            MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users));
698                    this.mUserPreviewAdapter.submitList(list);
699                });
700        this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
701        this.binding.showUsers.setVisibility(users.size() > 0 ? View.VISIBLE : View.GONE);
702        this.binding.showUsers.setText(
703                getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
704        this.binding.usersWrapper.setVisibility(
705                users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
706        if (users.size() == 0) {
707            this.binding.noUsersHints.setText(
708                    mucOptions.isPrivateAndNonAnonymous()
709                            ? R.string.no_users_hint_group_chat
710                            : R.string.no_users_hint_channel);
711            this.binding.noUsersHints.setVisibility(View.VISIBLE);
712        } else {
713            this.binding.noUsersHints.setVisibility(View.GONE);
714        }
715    }
716
717    public static String getStatus(Context context, User user, final boolean advanced) {
718        if (advanced) {
719            return String.format(
720                    "%s (%s)",
721                    context.getString(affiliationToStringRes(user.getAffiliation())),
722                    context.getString(roleToStringRes(user.getRole())));
723        } else {
724            return context.getString(affiliationToStringRes(user.getAffiliation()));
725        }
726    }
727
728    private static @StringRes int affiliationToStringRes(final Affiliation affiliation) {
729        return switch (affiliation) {
730            case OWNER -> R.string.owner;
731            case ADMIN -> R.string.admin;
732            case MEMBER -> R.string.member;
733            case NONE -> R.string.no_affiliation;
734            case OUTCAST -> R.string.outcast;
735        };
736    }
737
738    private static @StringRes int roleToStringRes(final Role role) {
739        return switch (role) {
740            case MODERATOR -> R.string.moderator;
741            case VISITOR -> R.string.visitor;
742            case PARTICIPANT -> R.string.participant;
743            case NONE -> R.string.no_role;
744        };
745    }
746
747    private String getStatus(User user) {
748        return getStatus(this, user, mAdvancedMode);
749    }
750
751    @Override
752    public void onAffiliationChangedSuccessful(Jid jid) {
753        refreshUi();
754    }
755
756    @Override
757    public void onAffiliationChangeFailed(Jid jid, int resId) {
758        displayToast(getString(resId, jid.asBareJid().toString()));
759    }
760
761    private void displayToast(final String msg) {
762        runOnUiThread(
763                () -> {
764                    if (isFinishing()) {
765                        return;
766                    }
767                    ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
768                });
769    }
770
771    @Override
772    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
773
774    @Override
775    public void onTextChanged(CharSequence s, int start, int before, int count) {}
776
777    @Override
778    public void afterTextChanged(Editable s) {
779        if (mConversation == null) {
780            return;
781        }
782        final MucOptions mucOptions = mConversation.getMucOptions();
783        if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
784            boolean subjectChanged =
785                    changed(
786                            binding.mucEditSubject.getEditableText().toString(),
787                            mucOptions.getSubject());
788            boolean nameChanged =
789                    changed(
790                            binding.mucEditTitle.getEditableText().toString(),
791                            mucOptions.getName());
792            if (subjectChanged || nameChanged) {
793                this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
794                this.binding.editMucNameButton.setContentDescription(getString(R.string.save));
795            } else {
796                this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
797                this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
798            }
799        }
800    }
801}