diff --git a/build.gradle b/build.gradle index 2778e7f76..2051cd41e 100644 --- a/build.gradle +++ b/build.gradle @@ -99,7 +99,7 @@ android { compileSdkVersion 33 defaultConfig { - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 33 versionCode 42058 versionName "2.12.5" diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 56e1ec02e..1dc78f02b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -109,6 +109,11 @@ public class Contact implements ListItem, Blockable { this.keys = new JSONObject(); } + public Contact(Contact other) { + this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar() == null ? null : other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability); + setAccount(other.getAccount()); + } + public static Contact fromCursor(final Cursor cursor) { final Jid jid; try { diff --git a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java index 3db6aa5a1..21cf7d80f 100644 --- a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java @@ -27,9 +27,10 @@ import eu.siacs.conversations.ui.adapter.ListItemAdapter; public abstract class AbstractSearchableListItemActivity extends XmppActivity implements TextView.OnEditorActionListener { protected ActivityChooseContactBinding binding; private final List listItems = new ArrayList<>(); - private ArrayAdapter mListItemsAdapter; + private ListItemAdapter mListItemsAdapter; - private EditText mSearchEditText; + protected MenuItem mMenuSearchView; + protected EditText mSearchEditText; private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { @@ -63,12 +64,12 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { + final int after) { } @Override public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { + final int count) { } }; @@ -84,7 +85,7 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im return mSearchEditText; } - public ArrayAdapter getListItemAdapter() { + public ListItemAdapter getListItemAdapter() { return mListItemsAdapter; } @@ -102,13 +103,13 @@ public abstract class AbstractSearchableListItemActivity extends XmppActivity im @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.choose_contact, menu); - final MenuItem menuSearchView = menu.findItem(R.id.action_search); - final View mSearchView = menuSearchView.getActionView(); + mMenuSearchView = menu.findItem(R.id.action_search); + final View mSearchView = mMenuSearchView.getActionView(); mSearchEditText = mSearchView.findViewById(R.id.search_field); mSearchEditText.addTextChangedListener(mSearchTextWatcher); mSearchEditText.setHint(R.string.search_contacts); mSearchEditText.setOnEditorActionListener(this); - menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); return true; } diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index 3377e6fe7..94f7aa9d7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -120,7 +120,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im multiple = intent.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false); if (multiple) { - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); getListView().setMultiChoiceModeListener(this); } @@ -136,6 +136,15 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im final SharedPreferences preferences = getPreferences(); this.startSearching = intent.getBooleanExtra("direct_search", false) && preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching)); + getListItemAdapter().refreshSettings(); + getListItemAdapter().setOnTagClickedListener((tag) -> { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(tag); + filterContacts(tag); + } + }); } private void onFabClicked(View v) { @@ -278,10 +287,19 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im getListItems().add(contact); } } + + final Contact self = new Contact(account.getSelfContact()); + self.setSystemName("Note to Self"); + if (self.match(this, needle)) { + getListItems().add(self); + } } } Collections.sort(getListItems()); getListItemAdapter().notifyDataSetChanged(); + for (int i = 0; i < getListItemAdapter().getCount(); i++) { + getListView().setItemChecked(i, selected.contains(getListItemAdapter().getItem(i).getJid().toString())); + } } private String[] getSelectedContactJids() { @@ -390,8 +408,24 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (multiple) { - startActionMode(this); - getListView().setItemChecked(position, true); + if (getListView().isItemChecked(position)) { + selected.add(getListItemAdapter().getItem(position).getJid().toString()); + } else { + selected.remove(getListItemAdapter().getItem(position).getJid().toString()); + } + + if (selected.isEmpty()) { + this.binding.fab.setImageResource(R.drawable.ic_person_add_white_24dp); + if (this.showEnterJid) { + this.binding.fab.show(); + } else { + this.binding.fab.hide(); + } + } else { + binding.fab.setImageResource(R.drawable.ic_forward_white_24dp); + binding.fab.show(); + } + return; } final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 4a1c56843..c0a0f5dbc 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -12,6 +12,7 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; @@ -21,6 +22,7 @@ import android.util.Pair; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -49,6 +51,8 @@ import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; @@ -58,9 +62,15 @@ import com.leinardi.android.speeddial.SpeedDialActionItem; import com.leinardi.android.speeddial.SpeedDialView; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -83,6 +93,7 @@ import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment; import eu.siacs.conversations.utils.AccountUtils; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; @@ -102,6 +113,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne private ListPagerAdapter mListPagerAdapter; private final List contacts = new ArrayList<>(); private ListItemAdapter mContactsAdapter; + private TagsAdapter mTagsAdapter = new TagsAdapter(); private final List conferences = new ArrayList<>(); private ListItemAdapter mConferenceAdapter; private final List mActivatedAccounts = new ArrayList<>(); @@ -703,6 +715,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne mSearchEditText = mSearchView.findViewById(R.id.search_field); mSearchEditText.addTextChangedListener(mSearchTextWatcher); mSearchEditText.setOnEditorActionListener(mSearchDone); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags)); + if (showDynamicTags) { + RecyclerView tags = mSearchView.findViewById(R.id.tags); + tags.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); + tags.setAdapter(mTagsAdapter); + } + String initialSearchValue = mInitialSearchValue.pop(); if (initialSearchValue != null) { mMenuSearchView.expandActionView(); @@ -996,7 +1017,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne protected void filterContacts(String needle) { this.contacts.clear(); + ArrayList tags = new ArrayList<>(); final List accounts = xmppConnectionService.getAccounts(); + boolean foundSopranica = false; for (Account account : accounts) { if (account.getStatus() != Account.State.DISABLED) { for (Contact contact : account.getRoster().getContacts()) { @@ -1006,11 +1029,52 @@ public class StartConversationActivity extends XmppActivity implements XmppConne || (needle != null && !needle.trim().isEmpty()) || s.compareTo(Presence.Status.OFFLINE) < 0)) { this.contacts.add(contact); + tags.addAll(contact.getTags(this)); + } + } + + final Contact self = new Contact(account.getSelfContact()); + self.setSystemName("Note to Self"); + if (self.match(this, needle)) { + this.contacts.add(self); + } + + for (Bookmark bookmark : account.getBookmarks()) { + if (bookmark.match(this, needle)) { + if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) { + foundSopranica = true; + } + this.contacts.add(bookmark); + tags.addAll(bookmark.getTags(this)); } } } } + + Comparator> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder()); + sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName()); + + mTagsAdapter.setTags( + tags.stream() + .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2)) + .entrySet().stream() + .sorted(sortTagsBy) + .map(e -> e.getKey()).collect(Collectors.toList()) + ); Collections.sort(this.contacts); + + final boolean sopranicaDeleted = getPreferences().getBoolean("cheogram_sopranica_bookmark_deleted", false); + + if (!sopranicaDeleted && !foundSopranica && (needle == null || needle.equals("")) && xmppConnectionService.getAccounts().size() > 0) { + Bookmark bookmark = new Bookmark( + xmppConnectionService.getAccounts().get(0), + Jid.of("discuss@conference.soprani.ca") + ); + bookmark.setBookmarkName("Soprani.ca / Cheogram Discussion"); + bookmark.addChild("group").setContent("support"); + this.contacts.add(0, bookmark); + } + mContactsAdapter.notifyDataSetChanged(); } @@ -1393,4 +1457,65 @@ public class StartConversationActivity extends XmppActivity implements XmppConne return false; } } + + + class TagsAdapter extends RecyclerView.Adapter { + class ViewHolder extends RecyclerView.ViewHolder { + protected TextView tv; + + public ViewHolder(View v) { + super(v); + tv = (TextView) v; + tv.setOnClickListener(view -> { + String needle = mSearchEditText.getText().toString(); + String tag = tv.getText().toString(); + String[] parts = needle.split("[,\\s]+"); + if(needle.isEmpty()) { + needle = tag; + } else if (tag.toLowerCase(Locale.US).contains(parts[parts.length-1])) { + needle = needle.replace(parts[parts.length-1], tag); + } else { + needle += ", " + tag; + } + mSearchEditText.setText(""); + mSearchEditText.append(needle); + filter(needle); + }); + } + + public void setTag(ListItem.Tag tag) { + tv.setText(tag.getName()); + tv.setBackgroundColor(tag.getColor()); + } + } + + protected List tags = new ArrayList<>(); + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { + View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_tag, null); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, int i) { + viewHolder.setTag(tags.get(i)); + } + + @Override + public int getItemCount() { + return tags.size(); + } + + public void setTags(final List tags) { + ListItem.Tag channelTag = new ListItem.Tag("Channel", UIHelper.getColorForName("Channel", true)); + String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim(); + HashSet parts = new HashSet<>(Arrays.asList(needle.split("[,\\s]+"))); + this.tags = tags.stream().filter( + tag -> !tag.equals(channelTag) && !parts.contains(tag.getName().toLowerCase(Locale.US)) + ).collect(Collectors.toList()); + if (!parts.contains("channel") && tags.contains(channelTag)) this.tags.add(0, channelTag); + notifyDataSetChanged(); + } + } } diff --git a/src/main/res/layout/actionview_search.xml b/src/main/res/layout/actionview_search.xml index 90783b776..725142c24 100644 --- a/src/main/res/layout/actionview_search.xml +++ b/src/main/res/layout/actionview_search.xml @@ -15,4 +15,10 @@ android:imeOptions="flagNoExtractUi|actionSearch" android:inputType="textEmailAddress|textNoSuggestions"/> + + \ No newline at end of file