diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 0552ce840..85a3e977d 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -456,7 +456,7 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public static Bundle defaultRoomConfiguration() { + public static Bundle defaultGroupChatConfiguration() { Bundle options = new Bundle(); options.putString("muc#roomconfig_persistentroom", "1"); options.putString("muc#roomconfig_membersonly", "1"); @@ -468,6 +468,18 @@ public class IqGenerator extends AbstractGenerator { return options; } + public static Bundle defaultChannelConfiguration() { + Bundle options = new Bundle(); + options.putString("muc#roomconfig_persistentroom", "1"); + options.putString("muc#roomconfig_membersonly", "0"); + options.putString("muc#roomconfig_publicroom", "1"); + options.putString("muc#roomconfig_whois", "moderators"); + options.putString("muc#roomconfig_enablearchiving", "1"); //prosody + options.putString("mam", "1"); //ejabberd community + options.putString("muc#roomconfig_mam","1"); //ejabberd saas + return options; + } + public IqPacket requestPubsubConfiguration(Jid jid, String node) { return pubsubConfiguration(jid, node, null); } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 3ae3179d5..dced15c94 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.parser; import android.util.Log; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; @@ -11,7 +10,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; @@ -99,7 +97,7 @@ public class PresenceParser extends AbstractParser implements +mucOptions.getConversation().getJid().asBareJid() +"' created. pushing default configuration"); mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(), - IqGenerator.defaultRoomConfiguration(), + IqGenerator.defaultGroupChatConfiguration(), null); } if (mXmppConnectionService.getPgpEngine() != null) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 2ee0d6062..ac2aeb49b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -2424,6 +2424,9 @@ public class XmppConnectionService extends Service { if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); updateConversationUi(); + if (onConferenceJoined != null) { + onConferenceJoined.onConferenceJoined(conversation); + } return; } @@ -2707,6 +2710,32 @@ public class XmppConnectionService extends Service { return null; } + + public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback callback) { + joinMuc(findOrCreateConversation(account, address, true, false, true), conversation -> { + final Bundle configuration = IqGenerator.defaultChannelConfiguration(); + if (!TextUtils.isEmpty(name)) { + configuration.putString("muc#roomconfig_roomname", name); + } + pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + saveConversationAsBookmark(conversation, name); + callback.success(conversation); + } + + @Override + public void onPushFailed() { + if (conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + callback.error(R.string.unable_to_set_channel_configuration, conversation); + } else { + callback.error(R.string.joined_an_existing_channel, conversation); + } + } + }); + }); + } + public boolean createAdhocConference(final Account account, final String name, final Iterable jids, @@ -2726,7 +2755,7 @@ public class XmppConnectionService extends Service { joinMuc(conversation, new OnConferenceJoined() { @Override public void onConferenceJoined(final Conversation conversation) { - final Bundle configuration = IqGenerator.defaultRoomConfiguration(); + final Bundle configuration = IqGenerator.defaultGroupChatConfiguration(); if (!TextUtils.isEmpty(name)) { configuration.putString("muc#roomconfig_roomname", name); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index adb36981c..7ba1c37a5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -474,6 +474,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } else { account = mConversation.getAccount().getJid().asBareJid().toString(); } + setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject()) ? View.VISIBLE : View.GONE); this.binding.detailsAccount.setText(getString(R.string.using_account, account)); this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString()); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 84a1bbe53..d5bc6a2a6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -952,6 +952,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (conversation.getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); + menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); } else { menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index f1957c6f2..5312f8212 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -55,6 +55,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.ui.interfaces.OnConversationArchived; import eu.siacs.conversations.ui.interfaces.OnConversationSelected; @@ -133,14 +134,23 @@ public class ConversationsOverviewFragment extends XmppFragment { if (activity instanceof OnConversationArchived) { ((OnConversationArchived) activity).onConversationArchived(swipedConversation.peek()); } - boolean isMuc = swipedConversation.peek().getMode() == Conversation.MODE_MULTI; - int title = isMuc ? R.string.title_undo_swipe_out_muc : R.string.title_undo_swipe_out_conversation; + final Conversation c = swipedConversation.peek(); + final int title; + if (c.getMode() == Conversational.MODE_MULTI) { + if (c.getMucOptions().isPrivateAndNonAnonymous()) { + title = R.string.title_undo_swipe_out_group_chat; + } else { + title = R.string.title_undo_swipe_out_channel; + } + } else { + title = R.string.title_undo_swipe_out_conversation; + } final Snackbar snackbar = Snackbar.make(binding.list, title, 5000) .setAction(R.string.undo, v -> { pendingActionHelper.undo(); - Conversation c = swipedConversation.pop(); - conversationsAdapter.insert(c, position); + Conversation conversation = swipedConversation.pop(); + conversationsAdapter.insert(conversation, position); if (formerlySelected) { if (activity instanceof OnConversationSelected) { ((OnConversationSelected) activity).onConversationSelected(c); @@ -167,9 +177,9 @@ public class ConversationsOverviewFragment extends XmppFragment { if (snackbar.isShownOrQueued()) { snackbar.dismiss(); } - Conversation c = swipedConversation.pop(); - if(c != null){ - if (!c.isRead() && c.getMode() == Conversation.MODE_SINGLE) { + final Conversation conversation = swipedConversation.pop(); + if(conversation != null){ + if (!conversation.isRead() && conversation.getMode() == Conversation.MODE_SINGLE) { return; } activity.xmppConnectionService.archiveConversation(c); diff --git a/src/main/java/eu/siacs/conversations/ui/CreateConferenceDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java similarity index 86% rename from src/main/java/eu/siacs/conversations/ui/CreateConferenceDialog.java rename to src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java index 21eee709e..ef2984a23 100644 --- a/src/main/java/eu/siacs/conversations/ui/CreateConferenceDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java @@ -1,18 +1,13 @@ package eu.siacs.conversations.ui; import android.app.Dialog; +import android.content.Context; import android.databinding.DataBindingUtil; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; import android.support.v7.app.AlertDialog; -import android.view.KeyEvent; -import android.view.View; -import android.widget.EditText; import android.widget.Spinner; -import android.widget.TextView; import java.util.ArrayList; import java.util.List; @@ -21,14 +16,14 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.CreateConferenceDialogBinding; import eu.siacs.conversations.ui.util.DelayedHintHelper; -public class CreateConferenceDialog extends DialogFragment { +public class CreatePrivateGroupChatDialog extends DialogFragment { private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; private CreateConferenceDialogListener mListener; - public static CreateConferenceDialog newInstance(List accounts) { - CreateConferenceDialog dialog = new CreateConferenceDialog(); - Bundle bundle = new Bundle(); + public static CreatePrivateGroupChatDialog newInstance(List accounts) { + CreatePrivateGroupChatDialog dialog = new CreatePrivateGroupChatDialog(); + Bundle bundle = new Bundle(); bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) accounts); dialog.setArguments(bundle); return dialog; @@ -44,7 +39,7 @@ public class CreateConferenceDialog extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.dialog_title_create_conference); + builder.setTitle(R.string.create_private_group_chat); CreateConferenceDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_conference_dialog, null, false); ArrayList mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY); StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account); @@ -59,6 +54,7 @@ public class CreateConferenceDialog extends DialogFragment { return builder.create(); } + public interface CreateConferenceDialogListener { void onCreateDialogPositiveClick(Spinner spinner, String subject); } diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java new file mode 100644 index 000000000..6b8c58cf3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java @@ -0,0 +1,297 @@ +package eu.siacs.conversations.ui; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.Spinner; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.CreateConferenceDialogBinding; +import eu.siacs.conversations.databinding.CreatePublicChannelDialogBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.ui.interfaces.OnBackendConnected; +import eu.siacs.conversations.ui.util.DelayedHintHelper; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.XmppConnection; +import rocks.xmpp.addr.Jid; + +public class CreatePublicChannelDialog extends DialogFragment implements OnBackendConnected { + + private static final char[] FORBIDDEN = new char[]{'\u0022','&','\'','/',':','<','>','@'}; + + private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; + private CreatePublicChannelDialogListener mListener; + private KnownHostsAdapter knownHostsAdapter; + private boolean jidWasModified = false; + private boolean nameEntered = false; + private boolean skipTetxWatcher = false; + private static final SecureRandom RANDOM = new SecureRandom(); + + public static CreatePublicChannelDialog newInstance(List accounts) { + CreatePublicChannelDialog dialog = new CreatePublicChannelDialog(); + Bundle bundle = new Bundle(); + bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) accounts); + dialog.setArguments(bundle); + return dialog; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setRetainInstance(true); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + jidWasModified = savedInstanceState != null && savedInstanceState.getBoolean("jid_was_modified_false", false); + nameEntered = savedInstanceState != null && savedInstanceState.getBoolean("name_entered", false); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.create_public_channel); + final CreatePublicChannelDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_public_channel_dialog, null, false); + binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + updateJidSuggestion(binding); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + binding.jid.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (skipTetxWatcher) { + return; + } + if (jidWasModified) { + jidWasModified = !TextUtils.isEmpty(s); + } else { + jidWasModified = !s.toString().equals(getJidSuggestion(binding)); + } + } + }); + updateInputs(binding,false); + ArrayList mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY); + StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account); + builder.setView(binding.getRoot()); + builder.setPositiveButton(nameEntered ? R.string.create : R.string.next, null); + builder.setNegativeButton(nameEntered ? R.string.back : R.string.cancel, null); + DelayedHintHelper.setHint(R.string.channel_bare_jid_example, binding.jid); + this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); + binding.jid.setAdapter(knownHostsAdapter); + final AlertDialog dialog = builder.create(); + binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> { + submit(dialog, binding); + return true; + }); + dialog.setOnShowListener(dialogInterface -> { + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> goBack(dialog, binding)); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> submit(dialog, binding)); + }); + return dialog; + } + + private void updateJidSuggestion(CreatePublicChannelDialogBinding binding) { + if (jidWasModified) { + return; + } + String jid = getJidSuggestion(binding); + skipTetxWatcher = true; + binding.jid.setText(jid); + skipTetxWatcher = false; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean("jid_was_modified",jidWasModified); + outState.putBoolean("name_entered", nameEntered); + super.onSaveInstanceState(outState); + } + + private static String getJidSuggestion(CreatePublicChannelDialogBinding binding) { + final Account account = StartConversationActivity.getSelectedAccount(binding.getRoot().getContext(), binding.account); + final XmppConnection connection = account == null ? null : account.getXmppConnection(); + if (connection == null) { + return ""; + } + final Editable nameText = binding.groupChatName.getText(); + final String name = nameText == null ? "" : nameText.toString().trim(); + final String domain = connection.getMucServer(); + if (domain == null) { + return ""; + } + final String localpart = clean(name); + if (TextUtils.isEmpty(localpart)) { + return ""; + } else { + try { + return Jid.of(localpart, domain, null).toEscapedString(); + } catch (IllegalArgumentException e) { + return Jid.of(CryptoHelper.pronounceable(RANDOM), domain, null).toEscapedString(); + } + } + } + + private static String clean(String name) { + for(char c : FORBIDDEN) { + name = name.replace(String.valueOf(c),""); + } + return name.replaceAll("\\s+","-"); + } + + private void goBack(AlertDialog dialog, CreatePublicChannelDialogBinding binding) { + if (nameEntered) { + nameEntered = false; + updateInputs(binding, true); + updateButtons(dialog); + } else { + dialog.dismiss(); + } + } + + private void submit(AlertDialog dialog, CreatePublicChannelDialogBinding binding) { + final Context context = binding.getRoot().getContext(); + final Editable nameText = binding.groupChatName.getText(); + final String name = nameText == null ? "" : nameText.toString().trim(); + final Editable addressText = binding.jid.getText(); + final String address = addressText == null ? "" : addressText.toString().trim(); + if (nameEntered) { + binding.nameLayout.setError(null); + if (address.isEmpty()) { + binding.xmppAddressLayout.setError(context.getText(R.string.please_enter_xmpp_address)); + } else { + final Jid jid; + try { + jid = Jid.ofEscaped(address); + } catch (IllegalArgumentException e) { + binding.xmppAddressLayout.setError(context.getText(R.string.invalid_jid)); + return; + } + final Account account = StartConversationActivity.getSelectedAccount(context, binding.account); + if (account == null) { + return; + } + final XmppConnectionService service = ((XmppActivity )context).xmppConnectionService; + if (service != null && service.findFirstMuc(jid) != null) { + binding.xmppAddressLayout.setError(context.getString(R.string.channel_already_exists)); + return; + } + mListener.onCreatePublicChannel(account, name, jid); + dialog.dismiss(); + } + } else { + binding.xmppAddressLayout.setError(null); + if (name.isEmpty()) { + binding.nameLayout.setError(context.getText(R.string.please_enter_name)); + } else if (StartConversationActivity.isValidJid(name)){ + binding.nameLayout.setError(context.getText(R.string.this_is_an_xmpp_address)); + } else { + binding.nameLayout.setError(null); + nameEntered = true; + updateInputs(binding, true); + updateButtons(dialog); + binding.jid.setText(""); + binding.jid.append(getJidSuggestion(binding)); + } + } + } + + + private void updateInputs(CreatePublicChannelDialogBinding binding, boolean requestFocus) { + binding.xmppAddressLayout.setVisibility(nameEntered ? View.VISIBLE : View.GONE); + binding.nameLayout.setVisibility(nameEntered ? View.GONE : View.VISIBLE); + if (!requestFocus) { + return; + } + if (nameEntered) { + binding.xmppAddressLayout.requestFocus(); + } else { + binding.nameLayout.requestFocus(); + } + } + + private void updateButtons(AlertDialog dialog) { + final Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + final Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + positive.setText(nameEntered ? R.string.create : R.string.next); + negative.setText(nameEntered ? R.string.back : R.string.cancel); + } + + @Override + public void onBackendConnected() { + refreshKnownHosts(); + } + + private void refreshKnownHosts() { + Activity activity = getActivity(); + if (activity instanceof XmppActivity) { + Collection hosts = ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts(); + this.knownHostsAdapter.refresh(hosts); + } + } + + public interface CreatePublicChannelDialogListener { + void onCreatePublicChannel(Account account, String name, Jid address); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mListener = (CreatePublicChannelDialogListener) context; + } catch (ClassCastException e) { + throw new ClassCastException(context.toString() + + " must implement CreateConferenceDialogListener"); + } + } + + @Override + public void onStart() { + super.onStart(); + final Activity activity = getActivity(); + if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { + refreshKnownHosts(); + } + } + + @Override + public void onDestroyView() { + Dialog dialog = getDialog(); + if (dialog != null && getRetainInstance()) { + dialog.setDismissMessage(null); + } + super.onDestroyView(); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java b/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java index 035e828b6..b9c9b15b1 100644 --- a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java @@ -50,9 +50,9 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.dialog_title_join_conference); + builder.setTitle(R.string.join_public_channel); DialogJoinConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_join_conference, null, false); - DelayedHintHelper.setHint(R.string.conference_address_example, binding.jid); + DelayedHintHelper.setHint(R.string.channel_full_jid_example, binding.jid); this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); binding.jid.setAdapter(knownHostsAdapter); String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index df78a6618..f0aebb573 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -6,7 +6,6 @@ import android.app.Dialog; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -14,27 +13,20 @@ import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.ListFragment; -import android.support.v4.content.ContextCompat; import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.Html; -import android.text.SpannableString; -import android.text.Spanned; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; @@ -56,9 +48,6 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import com.leinardi.android.speeddial.SpeedDialActionItem; -import com.leinardi.android.speeddial.SpeedDialView; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -78,12 +67,10 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.ListItemAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.service.EmojiService; import eu.siacs.conversations.ui.util.JidDialog; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.XmppUri; @@ -91,7 +78,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import rocks.xmpp.addr.Jid; -public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreateConferenceDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener { +public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri"; @@ -216,12 +203,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne return true; } }; - private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - onTabChanged(); - } - }; public static void populateAccountSpinner(Context context, List accounts, Spinner spinner) { if (accounts.size() > 0) { @@ -279,35 +260,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne Toolbar toolbar = (Toolbar) binding.toolbar; setSupportActionBar(toolbar); configureActionBar(getSupportActionBar()); - this.binding.speedDial.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - if (binding.startConversationViewPager.getCurrentItem() == 0) { - String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null; - if (searchString != null && !searchString.trim().isEmpty()) { - try { - Jid jid = Jid.of(searchString); - if (jid.getLocal() != null && jid.isBareJid() && jid.getDomain().contains(".")) { - showCreateContactDialog(jid.toString(), null); - return false; - } - } catch (IllegalArgumentException ignored) { - //ignore and fall through - } - } - showCreateContactDialog(null, null); - } - return false; - } - @Override - public void onToggleChanged(boolean isOpen) { - - } - }); + binding.speedDial.inflate(R.menu.start_conversation_fab_submenu); binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); - binding.startConversationViewPager.addOnPageChangeListener(mOnPageChangeListener); mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); binding.startConversationViewPager.setAdapter(mListPagerAdapter); @@ -342,18 +298,40 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission",false)); binding.speedDial.setOnActionSelectedListener(actionItem -> { + final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null; + final String prefilled; + if (isValidJid(searchString)) { + prefilled = Jid.ofEscaped(searchString).toEscapedString(); + } else { + prefilled = null; + } switch (actionItem.getId()) { - case R.id.enter: - showJoinConferenceDialog(null); + case R.id.join_public_channel: + showJoinConferenceDialog(prefilled); break; - case R.id.create: - showCreateConferenceDialog(); + case R.id.create_private_group_chat: + showCreatePrivateGroupChatDialog(); + break; + case R.id.create_public_channel: + showPublicChannelDialog(); + break; + case R.id.create_contact: + showCreateContactDialog(prefilled,null); break; } return false; }); } + public static boolean isValidJid(String input) { + try { + Jid jid = Jid.ofEscaped(input); + return !jid.isDomainJid(); + } catch (IllegalArgumentException e) { + return false; + } + } + @Override public void onSaveInstanceState(Bundle savedInstanceState) { Intent pendingIntent = pendingViewIntent.peek(); @@ -402,10 +380,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne switchToConversation(conversation); } - protected void openConversationForBookmark() { - openConversationForBookmark(conference_context_id); - } - protected void openConversationForBookmark(int position) { Bookmark bookmark = (Bookmark) conferences.get(position); openConversationsForBookmark(bookmark); @@ -504,7 +478,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.addToBackStack(null); EnterJidDialog dialog = EnterJidDialog.newInstance( mActivatedAccounts, - getString(R.string.dialog_title_create_contact), + getString(R.string.add_contact), getString(R.string.create), prefilledJid, null, @@ -554,32 +528,51 @@ public class StartConversationActivity extends XmppActivity implements XmppConne joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } - private void showCreateConferenceDialog() { + private void showCreatePrivateGroupChatDialog() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); - CreateConferenceDialog createConferenceFragment = CreateConferenceDialog.newInstance(mActivatedAccounts); + CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts); createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } - private Account getSelectedAccount(Spinner spinner) { - if (!spinner.isEnabled()) { + private void showPublicChannelDialog() { + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); + if (prev != null) { + ft.remove(prev); + } + ft.addToBackStack(null); + CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts); + dialog.show(ft, FRAGMENT_TAG_DIALOG); + } + + public static Account getSelectedAccount(Context context, Spinner spinner) { + if (spinner == null || !spinner.isEnabled()) { return null; } - Jid jid; - try { - if (Config.DOMAIN_LOCK != null) { - jid = Jid.of((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - jid = Jid.of((String) spinner.getSelectedItem()); + if (context instanceof XmppActivity) { + Jid jid; + try { + if (Config.DOMAIN_LOCK != null) { + jid = Jid.of((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null); + } else { + jid = Jid.of((String) spinner.getSelectedItem()); + } + } catch (final IllegalArgumentException e) { + return null; } - } catch (final IllegalArgumentException e) { + final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService; + if (service == null) { + return null; + } + return service.findAccountByJid(jid); + } else { return null; } - return xmppConnectionService.findAccountByJid(jid); } protected void switchToConversation(Contact contact) { @@ -944,19 +937,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne mConferenceAdapter.notifyDataSetChanged(); } - private void onTabChanged() { - @DrawableRes final int fabDrawable; - if (binding.startConversationViewPager.getCurrentItem() == 0) { - fabDrawable = R.drawable.ic_person_add_white_24dp; - binding.speedDial.clearActionItems(); - } else { - fabDrawable = R.drawable.ic_group_add_white_24dp; - binding.speedDial.inflate(R.menu.start_conversation_group_fab); - } - binding.speedDial.setMainFabClosedDrawable(ContextCompat.getDrawable(this,fabDrawable)); - invalidateOptionsMenu(); - } - @Override public void OnUpdateBlocklist(final Status status) { refreshUi(); @@ -996,7 +976,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (!xmppConnectionServiceBound) { return; } - final Account account = getSelectedAccount(spinner); + final Account account = getSelectedAccount(this, spinner); if (account == null) { return; } @@ -1014,7 +994,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (!xmppConnectionServiceBound) { return; } - final Account account = getSelectedAccount(spinner); + final Account account = getSelectedAccount(this, spinner); if (account == null) { return; } @@ -1073,6 +1053,35 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } } + @Override + public void onCreatePublicChannel(Account account, String name, Jid address) { + mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG); + mToast.show(); + xmppConnectionService.createPublicChannel(account, name, address, new UiCallback() { + @Override + public void success(Conversation conversation) { + runOnUiThread(() -> { + hideToast(); + switchToConversation(conversation); + }); + + } + + @Override + public void error(int errorCode, Conversation conversation) { + runOnUiThread(() -> { + replaceToast(getString(errorCode)); + switchToConversation(conversation); + }); + } + + @Override + public void userInputRequried(PendingIntent pi, Conversation object) { + + } + }); + } + public static class MyListFragment extends SwipeRefreshListFragment { private AdapterView.OnItemClickListener mOnItemClickListener; private int mResContextMenu; @@ -1154,9 +1163,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne case R.id.context_delete_contact: activity.deleteContact(); break; - case R.id.context_join_conference: - activity.openConversationForBookmark(); - break; case R.id.context_share_uri: activity.shareBookmarkUri(); break; @@ -1221,7 +1227,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne case 0: return getResources().getString(R.string.contacts); case 1: - return getResources().getString(R.string.conferences); + return getResources().getString(R.string.bookmarks); default: return super.getPageTitle(position); } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index dc71b328a..40b3cad65 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -63,7 +63,8 @@ public final class CryptoHelper { } public static String pronounceable(SecureRandom random) { - char[] output = new char[random.nextInt(4) * 2 + 5]; + final int rand = random.nextInt(4); + char[] output = new char[rand * 2 + (5 - rand)]; boolean vowel = random.nextBoolean(); for (int i = 0; i < output.length; ++i) { output[i] = vowel ? VOWELS[random.nextInt(VOWELS.length)] : CONSONANTS[random.nextInt(CONSONANTS.length)]; diff --git a/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/src/main/res/drawable-hdpi/ic_close_white_24dp.png deleted file mode 100644 index ceb1a1eeb..000000000 Binary files a/src/main/res/drawable-hdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_public_white_24dp.png b/src/main/res/drawable-hdpi/ic_public_white_24dp.png new file mode 100644 index 000000000..927aef38c Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_public_white_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/src/main/res/drawable-mdpi/ic_close_white_24dp.png deleted file mode 100644 index af7f8288d..000000000 Binary files a/src/main/res/drawable-mdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_public_white_24dp.png b/src/main/res/drawable-mdpi/ic_public_white_24dp.png new file mode 100644 index 000000000..d82023b6b Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_public_white_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/src/main/res/drawable-xhdpi/ic_close_white_24dp.png deleted file mode 100644 index b7c7ffd0e..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_public_white_24dp.png b/src/main/res/drawable-xhdpi/ic_public_white_24dp.png new file mode 100644 index 000000000..f545c6c1b Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_public_white_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png deleted file mode 100644 index 6b717e0dd..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_public_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_public_white_24dp.png new file mode 100644 index 000000000..123f22336 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_public_white_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png deleted file mode 100644 index 396419219..000000000 Binary files a/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-xxxhdpi/ic_public_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_public_white_24dp.png new file mode 100644 index 000000000..eb1097d41 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_public_white_24dp.png differ diff --git a/src/main/res/layout/activity_start_conversation.xml b/src/main/res/layout/activity_start_conversation.xml index e6651d174..b83f595b9 100644 --- a/src/main/res/layout/activity_start_conversation.xml +++ b/src/main/res/layout/activity_start_conversation.xml @@ -46,10 +46,10 @@ app:backgroundTint="?colorPrimary" android:layout_alignParentRight="true" android:layout_alignParentBottom="true" - app:sdMainFabClosedSrc="@drawable/ic_person_add_white_24dp" + app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" app:sdMainFabClosedBackgroundColor="?colorPrimary" - app:sdMainFabOpenedSrc="@drawable/ic_close_white_24dp" app:sdMainFabOpenedBackgroundColor="?colorPrimaryDark" + app:sdUseReverseAnimationOnClose="true" app:sdOverlayLayout="@id/overlay"/> diff --git a/src/main/res/layout/create_conference_dialog.xml b/src/main/res/layout/create_conference_dialog.xml index 6680ce246..f0e2f19f7 100644 --- a/src/main/res/layout/create_conference_dialog.xml +++ b/src/main/res/layout/create_conference_dialog.xml @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/create_dialog_group_chat_name" - android:imeOptions="actionNext"/> + android:imeOptions="actionNext|flagNoExtractUi"/> diff --git a/src/main/res/layout/create_public_channel_dialog.xml b/src/main/res/layout/create_public_channel_dialog.xml new file mode 100644 index 000000000..4e458fc3c --- /dev/null +++ b/src/main/res/layout/create_public_channel_dialog.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/layout/dialog_join_conference.xml b/src/main/res/layout/dialog_join_conference.xml index ba78f2682..cb8fd313d 100644 --- a/src/main/res/layout/dialog_join_conference.xml +++ b/src/main/res/layout/dialog_join_conference.xml @@ -26,7 +26,7 @@ android:id="@+id/account_jid_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/conference_address" + android:hint="@string/xmpp_address" app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> @@ -36,7 +36,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" - android:imeOptions="actionDone"/> + android:imeOptions="actionDone|flagNoExtractUi"/> + android:imeOptions="actionDone|flagNoExtractUi"/> diff --git a/src/main/res/menu/conference_context.xml b/src/main/res/menu/conference_context.xml index ee9517ed8..725a7590e 100644 --- a/src/main/res/menu/conference_context.xml +++ b/src/main/res/menu/conference_context.xml @@ -1,9 +1,6 @@ - diff --git a/src/main/res/menu/start_conversation_fab_submenu.xml b/src/main/res/menu/start_conversation_fab_submenu.xml new file mode 100644 index 000000000..76a576d6a --- /dev/null +++ b/src/main/res/menu/start_conversation_fab_submenu.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/start_conversation_group_fab.xml b/src/main/res/menu/start_conversation_group_fab.xml deleted file mode 100644 index 3c1bb2a42..000000000 --- a/src/main/res/menu/start_conversation_group_fab.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index bf0c7d662..de27b1b51 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Close this conversation Contact details Group chat details + Channel details Secure conversation Add account Edit name @@ -211,12 +212,9 @@ Fetching keys… Done Decrypt - Group chats + Bookmarks Search - Create Contact Enter Contact - Join group chat - Join Group Chat Delete contact View contact details Block contact @@ -225,8 +223,8 @@ Select The contact already exists Join - Group chat address - room@conference.example.com/nick + channel@conference.example.com/nick + channel@conference.example.com Save as bookmark Delete bookmark Destroy group chat @@ -416,7 +414,8 @@ No application found to display location Location Conversation closed - Left group chat + Left private group chat + Left public channel Don’t trust system CAs All certificates must be manually approved Remove certificates @@ -541,7 +540,6 @@ Your device does not support opting out of battery optimization Registration failed: Try again later Registration failed: Password too weak - Create Group Chat Choose participants Creating group chat… Invite again @@ -824,4 +822,16 @@ Backup & Restore Enter Jabber ID Create group chat + Join public channel + Create private group chat + Create public channel + Channel name + XMPP address + Please provide a name for the channel + Please provide an XMPP address + This is an XMPP address. Please provide a name. + Creating public channel… + This channel already exits + You’ve joined an existing channel + Unable to set channel configuration diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index f7522f284..e7bc3cd31 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -10,7 +10,7 @@ @color/grey50 @color/grey200 @color/grey300 - @color/grey300_40 + @color/black12 @color/red_a700 @color/green600 @color/red800 @@ -123,7 +123,7 @@ @color/grey800 @color/grey900 @color/grey700 - @color/grey700_40 + @color/black26 @drawable/search_background_dark @drawable/no_results_background_dark @drawable/list_item_background_dark