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}