Merge branch 'start-join'

Stephen Paul Weber created

* start-join:
  Unify add contact and join channel
  If a MUC address is entered as a contact, join it

Change summary

src/cheogram/res/values/strings.xml                                      |  1 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 12 
src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java           |  6 
src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java       |  6 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  3 
src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java              | 55 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java   | 75 
src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java          |  6 
src/main/java/eu/siacs/conversations/utils/Consumer.java                 |  6 
src/main/res/layout/enter_jid_dialog.xml                                 |  9 
src/main/res/menu/start_conversation_fab_submenu.xml                     |  6 
11 files changed, 132 insertions(+), 53 deletions(-)

Detailed changes

src/cheogram/res/values/strings.xml 🔗

@@ -35,4 +35,5 @@
     <string name="moderate_reason">Moderation Reason</string>
     <string name="unable_to_moderate">Unable to Moderate</string>
     <string name="block_media">Block Media</string>
+    <string name="add_contact">New Contact or Channel</string>
 </resources>

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -136,6 +136,7 @@ import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.utils.Consumer;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.EasyOnboardingInvite;
@@ -3432,6 +3433,17 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
+        IqPacket request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
+        sendIqPacket(account, request, (acct, reply) -> {
+            ServiceDiscoveryResult result = new ServiceDiscoveryResult(reply);
+            cb.accept(
+                result.getFeatures().contains("http://jabber.org/protocol/muc") &&
+                result.hasIdentity("conference", null)
+            );
+        });
+    }
+
     public void fetchConferenceConfiguration(final Conversation conversation) {
         fetchConferenceConfiguration(conversation, null);
     }

src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java 🔗

@@ -80,12 +80,14 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
 				getString(R.string.block_jabber_id),
 				getString(R.string.block),
 				null,
+				null,
 				account.getJid().asBareJid().toEscapedString(),
 				true,
-				false
+				false,
+				EnterJidDialog.SanityCheck.NO
 		);
 
-		dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
+		dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
 			Blockable blockable = new RawBlockable(account, contactJid);
 			if (xmppConnectionService.sendBlockRequest(blockable, false)) {
 				Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();

src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java 🔗

@@ -326,13 +326,15 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
                 mActivatedAccounts,
                 getString(R.string.enter_contact),
                 getString(R.string.select),
+                null,
                 jid == null ? null : jid.asBareJid().toString(),
                 getIntent().getStringExtra(EXTRA_ACCOUNT),
                 true,
-                false
+                false,
+                EnterJidDialog.SanityCheck.NO
         );
 
-        dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
+        dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
             final Intent request = getIntent();
             final Intent data = new Intent();
             data.putExtra("contact", contactJid.toString());

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -2910,6 +2910,9 @@ public class ConversationFragment extends XmppFragment
             attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false);
             return;
         }
+        if ("call".equals(postInitAction)) {
+            checkPermissionAndTriggerAudioCall();
+        }
         if ("message".equals(postInitAction)) {
             binding.conversationViewPager.post(() -> {
                 binding.conversationViewPager.setCurrentItem(0);

src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java 🔗

@@ -57,39 +57,51 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
 
     private static final String TITLE_KEY = "title";
     private static final String POSITIVE_BUTTON_KEY = "positive_button";
+    private static final String SECONDARY_BUTTON_KEY = "secondary_button";
     private static final String PREFILLED_JID_KEY = "prefilled_jid";
     private static final String ACCOUNT_KEY = "account";
     private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
     private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
     private static final String SANITY_CHECK_JID = "sanity_check_jid";
+    private static final String SHOW_BOOKMARK_CHECKBOX = "show_bookmark_checkbox";
 
     private KnownHostsAdapter knownHostsAdapter;
     private Collection<String> whitelistedDomains = Collections.emptyList();
 
     private EnterJidDialogBinding binding;
     private AlertDialog dialog;
-    private boolean sanityCheckJid = false;
+    private SanityCheck sanityCheckJid = SanityCheck.NO;
 
     private boolean issuedWarning = false;
     private GatewayListAdapter gatewayListAdapter = new GatewayListAdapter();
 
+    public static enum SanityCheck {
+        NO,
+        YES,
+        ALLOW_MUC
+    }
+
     public static EnterJidDialog newInstance(
             final List<String> activatedAccounts,
             final String title,
             final String positiveButton,
+            final String secondaryButton,
             final String prefilledJid,
             final String account,
             boolean allowEditJid,
-            final boolean sanity_check_jid) {
+            boolean showBookmarkCheckbox,
+            final SanityCheck sanity_check_jid) {
         EnterJidDialog dialog = new EnterJidDialog();
         Bundle bundle = new Bundle();
         bundle.putString(TITLE_KEY, title);
         bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
+        bundle.putString(SECONDARY_BUTTON_KEY, secondaryButton);
         bundle.putString(PREFILLED_JID_KEY, prefilledJid);
         bundle.putString(ACCOUNT_KEY, account);
         bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
         bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts);
-        bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
+        bundle.putInt(SANITY_CHECK_JID, sanity_check_jid.ordinal());
+        bundle.putBoolean(SHOW_BOOKMARK_CHECKBOX, showBookmarkCheckbox);
         dialog.setArguments(bundle);
         return dialog;
     }
@@ -131,7 +143,11 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
                 binding.jid.setCursorVisible(false);
             }
         }
-        sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
+        sanityCheckJid = SanityCheck.values()[getArguments().getInt(SANITY_CHECK_JID, SanityCheck.NO.ordinal())];
+
+        if (!getArguments().getBoolean(SHOW_BOOKMARK_CHECKBOX, false)) {
+            binding.bookmark.setVisibility(View.GONE);
+        }
 
         DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
 
@@ -185,23 +201,26 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
         });
 
         builder.setView(binding.getRoot());
-        builder.setNegativeButton(R.string.cancel, null);
         builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
+        if (getArguments().getString(SECONDARY_BUTTON_KEY) == null) {
+            builder.setNegativeButton(R.string.cancel, null);
+        } else {
+            builder.setNegativeButton(getArguments().getString(SECONDARY_BUTTON_KEY), null);
+            builder.setNeutralButton(R.string.cancel, null);
+        }
         this.dialog = builder.create();
 
-        View.OnClickListener dialogOnClick =
-                v -> {
-                    handleEnter(binding, account);
-                };
-
         binding.jid.setOnEditorActionListener(
                 (v, actionId, event) -> {
-                    handleEnter(binding, account);
+                    handleEnter(binding, account, false);
                     return true;
                 });
 
         dialog.show();
-        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
+        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((v) -> handleEnter(binding, account, false));
+        if (getArguments().getString(SECONDARY_BUTTON_KEY) != null) {
+            dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener((v) -> handleEnter(binding, account, true));
+        }
         return dialog;
     }
 
@@ -217,7 +236,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
         }
     }
 
-    private void handleEnter(EnterJidDialogBinding binding, String account) {
+    private void handleEnter(EnterJidDialogBinding binding, String account, boolean secondary) {
         if (!binding.account.isEnabled() && account == null) {
             return;
         }
@@ -236,7 +255,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
                     return;
                 }
 
-                final Jid contactJid;
+                Jid contactJid = null;
                 try {
                     contactJid = Jid.ofEscaped(jidString);
                 } catch (final IllegalArgumentException e) {
@@ -244,14 +263,14 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
                     return;
                 }
 
-                if (!issuedWarning && sanityCheckJid) {
+                if (!issuedWarning && sanityCheckJid != SanityCheck.NO) {
                     if (contactJid.isDomainJid()) {
                         binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain));
                         dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
                         issuedWarning = true;
                         return;
                     }
-                    if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
+                    if (sanityCheckJid != SanityCheck.ALLOW_MUC && suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
                         binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel));
                         dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
                         issuedWarning = true;
@@ -261,7 +280,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
 
                 if (mListener != null) {
                     try {
-                        if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
+                        if (mListener.onEnterJidDialogPositive(accountJid, contactJid, secondary, binding.bookmark.isChecked())) {
                             dialog.dismiss();
                         }
                     } catch (JidError error) {
@@ -335,7 +354,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
     }
 
     public interface OnEnterJidDialogPositiveListener {
-        boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
+        boolean onEnterJidDialogPositive(Jid account, Jid contact, boolean secondary, boolean save) throws EnterJidDialog.JidError;
     }
 
     public static class JidError extends Exception {

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -339,9 +339,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                 case R.id.discover_public_channels:
                     startActivity(new Intent(this, ChannelDiscoveryActivity.class));
                     break;
-                case R.id.join_public_channel:
-                    showJoinConferenceDialog(prefilled);
-                    break;
                 case R.id.create_private_group_chat:
                     showCreatePrivateGroupChatDialog();
                     break;
@@ -531,15 +528,17 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         ft.addToBackStack(null);
         EnterJidDialog dialog = EnterJidDialog.newInstance(
                 mActivatedAccounts,
-                getString(R.string.add_contact),
-                getString(R.string.add),
+                getString(R.string.start_conversation),
+                getString(R.string.message),
+                "Call",
                 prefilledJid,
                 invite == null ? null : invite.account,
                 invite == null || !invite.hasFingerprints(),
-                true
+                true,
+                EnterJidDialog.SanityCheck.ALLOW_MUC
         );
 
-        dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
+        dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, call, save) -> {
             if (!xmppConnectionServiceBound) {
                 return false;
             }
@@ -548,25 +547,55 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             if (account == null) {
                 return true;
             }
-
             final Contact contact = account.getRoster().getContact(contactJid);
+
             if (invite != null && invite.getName() != null) {
                 contact.setServerName(invite.getName());
             }
-            if (contact.isSelf()) {
-                switchToConversation(contact);
-                return true;
-            } else if (contact.showInRoster()) {
-                throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
-            } else {
-                final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
-                xmppConnectionService.createContact(contact, true, preAuth);
-                if (invite != null && invite.hasFingerprints()) {
-                    xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
-                }
-                switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody());
+
+            if (contact.isSelf() || contact.showInRoster()) {
+                switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
                 return true;
             }
+
+            xmppConnectionService.checkIfMuc(account, contactJid, (isMuc) -> {
+                if (isMuc) {
+                    if (save) {
+                        Bookmark bookmark = account.getBookmark(contactJid);
+                        if (bookmark != null) {
+                            openConversationsForBookmark(bookmark);
+                        } else {
+                            bookmark = new Bookmark(account, contactJid.asBareJid());
+                            bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
+                            final String nick = contactJid.getResource();
+                            if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
+                                bookmark.setNick(nick);
+                            }
+                            xmppConnectionService.createBookmark(account, bookmark);
+                            final Conversation conversation = xmppConnectionService
+                                    .findOrCreateConversation(account, contactJid, true, true, true);
+                            bookmark.setConversation(conversation);
+                            switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
+                        }
+                    } else {
+                        final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, true, true, true);
+                        switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
+                    }
+                } else {
+                    if (save) {
+                        final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
+                        xmppConnectionService.createContact(contact, true, preAuth);
+                        if (invite != null && invite.hasFingerprints()) {
+                            xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
+                        }
+                    }
+                    switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
+                }
+
+                dialog.dismiss();
+            });
+
+            return false;
         });
         dialog.show(ft, FRAGMENT_TAG_DIALOG);
     }
@@ -636,8 +665,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     protected void switchToConversationDoNotAppend(Contact contact, String body) {
+        switchToConversationDoNotAppend(contact, body, null);
+    }
+
+    protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
         Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
-        switchToConversationDoNotAppend(conversation, body);
+        switchToConversation(conversation, body, false, null, false, true, postInit);
     }
 
     @Override

src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java 🔗

@@ -26,6 +26,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.databinding.DataBindingUtil;
 import eu.siacs.conversations.databinding.DialpadBinding;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.utils.Consumer;
 
 public class DialpadView extends ConstraintLayout implements View.OnClickListener {
 
@@ -64,9 +65,4 @@ public class DialpadView extends ConstraintLayout implements View.OnClickListene
     public void onClick(View v) {
         clickConsumer.accept(v.getTag().toString());
     }
-
-	// Based on java.util.function.Consumer to avoid Android 24 dependency
-	public interface Consumer<T> {
-		void accept(T t);
-	}
 }

src/main/res/layout/enter_jid_dialog.xml 🔗

@@ -43,5 +43,14 @@
                 android:imeOptions="actionDone|flagNoExtractUi"
                 android:inputType="textEmailAddress" />
         </com.google.android.material.textfield.TextInputLayout>
+
+        <CheckBox
+            android:id="@+id/bookmark"
+            style="@style/Widget.Conversations.CheckBox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:checked="true"
+            android:text="Save as Contact / Bookmark"/>
     </LinearLayout>
 </layout>

src/main/res/menu/start_conversation_fab_submenu.xml 🔗

@@ -4,10 +4,6 @@
         android:id="@+id/discover_public_channels"
         android:icon="@drawable/ic_search_white_24dp"
         android:title="@string/discover_channels" />
-    <item
-        android:id="@+id/join_public_channel"
-        android:icon="@drawable/ic_input_white_24dp"
-        android:title="@string/join_public_channel" />
     <item
         android:id="@+id/create_public_channel"
         android:icon="@drawable/ic_public_white_24dp"
@@ -20,4 +16,4 @@
         android:id="@+id/create_contact"
         android:icon="@drawable/ic_person_white_48dp"
         android:title="@string/add_contact" />
-</menu>
+</menu>