From bcf073038753caf47225c859fa0ece4711ae07b6 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 30 May 2022 15:18:33 -0500 Subject: [PATCH 01/50] List commands when available If a JID advertises commands, list them. This only works if the JID supports CAPS and is in our roster and we have already fetched CAPS for the resource. --- .../conversations/generator/IqGenerator.java | 9 +- .../services/XmppConnectionService.java | 5 + .../ui/ConversationFragment.java | 79 ++++++ .../ui/adapter/CommandAdapter.java | 27 ++ src/main/res/layout/command_row.xml | 36 +++ src/main/res/layout/fragment_conversation.xml | 251 ++++++++++-------- 6 files changed, 300 insertions(+), 107 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java create mode 100644 src/main/res/layout/command_row.xml diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 2776aa4f058aef2fa4d0a292b3c6c1f452123abd..ed831d784628f43e6d38ef0ce4d72d915b15a668 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -552,7 +552,14 @@ public class IqGenerator extends AbstractGenerator { public IqPacket queryDiscoItems(Jid jid) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(jid); - packet.addChild("query",Namespace.DISCO_ITEMS); + packet.query(Namespace.DISCO_ITEMS); + return packet; + } + + public IqPacket queryDiscoItems(Jid jid, String node) { + IqPacket packet = queryDiscoItems(jid); + final Element query = packet.query(Namespace.DISCO_ITEMS); + query.setAttribute("node", node); return packet; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 7a4d6733b0b8f740761307114de51051651d604a..afecec477c8b091753231e05e8d229e855f6f725 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4744,6 +4744,11 @@ public class XmppConnectionService extends Service { } } + public void fetchCommands(Account account, final Jid jid, OnIqPacketReceived callback) { + final IqPacket request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands"); + sendIqPacket(account, request, callback); + } + private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { boolean rosterNeedsSync = false; for (final Contact contact : roster.getContacts()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index c60557cceb9b8bb59365ea17db6c03ceddd5b774..da24f5a344dcf024e0ed21fadbcee821b9807b78 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -55,11 +55,14 @@ import android.widget.Toast; import androidx.annotation.IdRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.databinding.DataBindingUtil; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; @@ -73,6 +76,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -92,6 +96,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; @@ -100,6 +105,7 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.adapter.CommandAdapter; import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.util.ActivityResult; @@ -129,6 +135,7 @@ import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; @@ -139,6 +146,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, @@ -185,6 +193,7 @@ public class ConversationFragment extends XmppFragment private final PendingItem pendingMessage = new PendingItem<>(); public Uri mPendingEditorContent = null; protected MessageAdapter messageListAdapter; + protected CommandAdapter commandAdapter; private MediaPreviewAdapter mediaPreviewAdapter; private String lastMessageUuid = null; private Conversation conversation; @@ -1234,6 +1243,39 @@ public class ConversationFragment extends XmppFragment new EditMessageActionModeCallback(this.binding.textinput)); } + binding.conversationViewPager.setAdapter(new StaticPagerAdapter( + binding.conversationViewPager + )); + binding.tabLayout.setupWithViewPager(binding.conversationViewPager); + + commandAdapter = new CommandAdapter((XmppActivity) getActivity()); + binding.commandsView.setAdapter(commandAdapter); + Presences presences = conversation.getContact().getPresences(); + for (Map.Entry entry : presences.getPresencesMap().entrySet()) { + String resource = entry.getKey(); + Presence presence = entry.getValue(); + if (presence.getServiceDiscoveryResult().getFeatures().contains("http://jabber.org/protocol/commands")) { + binding.tabLayout.setVisibility(View.VISIBLE); + binding.conversationViewPager.setCurrentItem(1); + Jid jid = conversation.getContact().getJid(); + if (resource != null && !resource.equals("")) jid = jid.withResource(resource); + activity.xmppConnectionService.fetchCommands(conversation.getAccount(), jid, (a, iq) -> { + if (iq.getType() == IqPacket.TYPE.RESULT) { + activity.runOnUiThread(() -> { + for (Element child : iq.query().getChildren()) { + if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue; + commandAdapter.add(child); + } + }); + } else { + binding.tabLayout.setVisibility(View.GONE); + binding.conversationViewPager.setCurrentItem(0); + } + }); + break; + } + } + return binding.getRoot(); } @@ -3604,4 +3646,41 @@ public class ConversationFragment extends XmppFragment } return activity; } + + public class StaticPagerAdapter extends PagerAdapter { + ViewPager mPager; + + StaticPagerAdapter(ViewPager pager) { + mPager = pager; + } + + @NonNull + @Override + public View instantiateItem(@NonNull ViewGroup container, int position) { + return mPager.getChildAt(position); + } + + @Override + public int getCount() { + return 2; + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { + return view == o; + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return "Conversation"; + case 1: + return "Commands"; + default: + return super.getPageTitle(position); + } + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..1b134f9f95755b15f9b3d9a58a6c07f53a8cbef6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java @@ -0,0 +1,27 @@ +package eu.siacs.conversations.ui.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.databinding.CommandRowBinding; + +public class CommandAdapter extends ArrayAdapter { + public CommandAdapter(XmppActivity activity) { + super(activity, 0); + } + + @Override + public View getView(int position, View view, @NonNull ViewGroup parent) { + CommandRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_row, parent, false); + binding.command.setText(getItem(position).getAttribute("name")); + return binding.getRoot(); + } +} diff --git a/src/main/res/layout/command_row.xml b/src/main/res/layout/command_row.xml new file mode 100644 index 0000000000000000000000000000000000000000..dd1ffbdb134e660155df273766da2f7cdb582808 --- /dev/null +++ b/src/main/res/layout/command_row.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 4b862b7525601f7c938a643fa349ac9cf988d499..945aca1ef9e31ab3ff44a23517cd3985f0be1580 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -7,123 +7,162 @@ android:layout_height="match_parent" android:background="?attr/color_background_secondary"> - + + - - - - - - - + - + + - - + + + + + + + + + + + + + + + + + + + + app:backgroundTint="?attr/color_background_primary" + app:fabSize="mini" + app:useCompatPadding="true" /> + + + - + - - - - - - - - - + - \ No newline at end of file + From d8051973a5f9f1e439c6f7263a6b24ba8adf289e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 31 May 2022 09:53:41 -0500 Subject: [PATCH 02/50] Default to conversation tab if there are unread messages --- .../siacs/conversations/entities/Contact.java | 7 ++++ .../conversations/entities/Conversation.java | 9 ++++ .../conversations/entities/Presences.java | 12 ++++++ .../ui/ConversationFragment.java | 41 ++++++++----------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index ffdd73cf3bec626686b99b4a5500ff90d592ae21..cb66538c55ea8ffb13968334997d26cfe23cc8f7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -292,6 +292,13 @@ public class Contact implements ListItem, Blockable { return this.presences.getShownStatus(); } + public Jid resourceWhichSupport(final String namespace) { + final String resource = getPresences().firstWhichSupport(namespace); + if (resource == null) return null; + + return resource.equals("") ? getJid() : getJid().withResource(resource); + } + public boolean setPhotoUri(String uri) { if (uri != null && !uri.equals(this.photoUri)) { this.photoUri = uri; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8bb65cc0f57ab3f0992f80c8f3ce41d0baba12c8..3e6cdcaf8d57c6f9a9e520b8636cb9ea2efa7589 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -30,6 +30,7 @@ import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.mam.MamReference; @@ -1109,6 +1110,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return getName().toString(); } + public int getCurrentTab() { + if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) { + return 0; + } + + return 1; + } + public interface OnMessageFound { void onMessageFound(final Message message); } diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index 4753e2138f7e4949a00d2180e2253f5434c2a78f..97560bd2d1dd9809011a6a10995dace11ef07361 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -149,6 +149,18 @@ public class Presences { return false; } + public String firstWhichSupport(final String namespace) { + for (Map.Entry entry : this.presences.entrySet()) { + String resource = entry.getKey(); + Presence presence = entry.getValue(); + if (presence.getServiceDiscoveryResult().getFeatures().contains(namespace)) { + return resource; + } + } + + return null; + } + public boolean anyIdentity(final String category, final String type) { synchronized (this.presences) { if (this.presences.size() == 0) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index da24f5a344dcf024e0ed21fadbcee821b9807b78..6d450de861e6094ceb232e4aa983a81b4aaa2ab6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1247,33 +1247,26 @@ public class ConversationFragment extends XmppFragment binding.conversationViewPager )); binding.tabLayout.setupWithViewPager(binding.conversationViewPager); + binding.conversationViewPager.setCurrentItem(conversation.getCurrentTab()); commandAdapter = new CommandAdapter((XmppActivity) getActivity()); binding.commandsView.setAdapter(commandAdapter); - Presences presences = conversation.getContact().getPresences(); - for (Map.Entry entry : presences.getPresencesMap().entrySet()) { - String resource = entry.getKey(); - Presence presence = entry.getValue(); - if (presence.getServiceDiscoveryResult().getFeatures().contains("http://jabber.org/protocol/commands")) { - binding.tabLayout.setVisibility(View.VISIBLE); - binding.conversationViewPager.setCurrentItem(1); - Jid jid = conversation.getContact().getJid(); - if (resource != null && !resource.equals("")) jid = jid.withResource(resource); - activity.xmppConnectionService.fetchCommands(conversation.getAccount(), jid, (a, iq) -> { - if (iq.getType() == IqPacket.TYPE.RESULT) { - activity.runOnUiThread(() -> { - for (Element child : iq.query().getChildren()) { - if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue; - commandAdapter.add(child); - } - }); - } else { - binding.tabLayout.setVisibility(View.GONE); - binding.conversationViewPager.setCurrentItem(0); - } - }); - break; - } + Jid commandJid = conversation.getContact().resourceWhichSupport(Namespace.COMMANDS); + if (commandJid != null) { + binding.tabLayout.setVisibility(View.VISIBLE); + activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> { + if (iq.getType() == IqPacket.TYPE.RESULT) { + activity.runOnUiThread(() -> { + for (Element child : iq.query().getChildren()) { + if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue; + commandAdapter.add(child); + } + }); + } else { + binding.tabLayout.setVisibility(View.GONE); + binding.conversationViewPager.setCurrentItem(0); + } + }); } return binding.getRoot(); From f9038107f7f6b8b6561dabaa5dc4e96510f6bd62 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 31 May 2022 10:17:18 -0500 Subject: [PATCH 03/50] Return to previously selected tab when opening a conversation If user manually selects a tab, remember that. --- .../eu/siacs/conversations/entities/Conversation.java | 7 +++++++ .../eu/siacs/conversations/ui/ConversationFragment.java | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 3e6cdcaf8d57c6f9a9e520b8636cb9ea2efa7589..c53a4e304fa3ed618993f932b645edf12a080db4 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -85,6 +85,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; private String mFirstMamReference = null; + protected int mCurrentTab = -1; public Conversation(final String name, final Account account, final Jid contactJid, final int mode) { @@ -1110,7 +1111,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return getName().toString(); } + public void setCurrentTab(int tab) { + mCurrentTab = tab; + } + public int getCurrentTab() { + if (mCurrentTab >= 0) return mCurrentTab; + if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) { return 0; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 6d450de861e6094ceb232e4aa983a81b4aaa2ab6..bd4ead7ea2b004d5a94e868665894d1c54e080b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1248,6 +1248,14 @@ public class ConversationFragment extends XmppFragment )); binding.tabLayout.setupWithViewPager(binding.conversationViewPager); binding.conversationViewPager.setCurrentItem(conversation.getCurrentTab()); + binding.conversationViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + public void onPageScrollStateChanged(int state) { } + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } + + public void onPageSelected(int position) { + conversation.setCurrentTab(position); + } + }); commandAdapter = new CommandAdapter((XmppActivity) getActivity()); binding.commandsView.setAdapter(commandAdapter); From 3bb2b7b791d61559d8090fc63e89eee7bc689fc0 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 31 May 2022 21:08:51 -0500 Subject: [PATCH 04/50] Execute command and show note or error response --- src/cheogram/res/layout/command_note.xml | 30 +++ src/cheogram/res/layout/command_page.xml | 27 ++ .../conversations/entities/Conversation.java | 253 ++++++++++++++++++ .../ui/ConversationFragment.java | 55 +--- src/main/res/layout/fragment_conversation.xml | 2 +- 5 files changed, 315 insertions(+), 52 deletions(-) create mode 100644 src/cheogram/res/layout/command_note.xml create mode 100644 src/cheogram/res/layout/command_page.xml diff --git a/src/cheogram/res/layout/command_note.xml b/src/cheogram/res/layout/command_note.xml new file mode 100644 index 0000000000000000000000000000000000000000..79c4f35fd18002ba1ff868acbf9e55aae0a8dc8a --- /dev/null +++ b/src/cheogram/res/layout/command_note.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/src/cheogram/res/layout/command_page.xml b/src/cheogram/res/layout/command_page.xml new file mode 100644 index 0000000000000000000000000000000000000000..c624dd8f8b016b26143952cbfa78a06465254dd9 --- /dev/null +++ b/src/cheogram/res/layout/command_page.xml @@ -0,0 +1,27 @@ + + + + + + +