diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 3bd33f660..5b9665455 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -68,6 +68,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; public static final String OMEMO_SETTING = "omemo"; public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; + public static final String GROUP_BY_TAGS = "groupByTags"; public static final int REQUEST_CREATE_BACKUP = 0xbf8701; @@ -536,6 +537,25 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference if (xmppConnectionService.reconfigurePushDistributor()) { xmppConnectionService.renewUnifiedPushEndpoints(); } + } else if (name.equals(SHOW_DYNAMIC_TAGS) || name.equals(GROUP_BY_TAGS)) { + boolean dynamicTagsEnabled = preferences.getBoolean(SHOW_DYNAMIC_TAGS, false); + boolean groupByTags = preferences.getBoolean(GROUP_BY_TAGS, false); + + if (name.equals(SHOW_DYNAMIC_TAGS) && !dynamicTagsEnabled && groupByTags) { + preferences.edit().putBoolean(GROUP_BY_TAGS, false).apply(); + Preference preference = mSettingsFragment.findPreference(GROUP_BY_TAGS); + if (preference instanceof CheckBoxPreference) { + ((CheckBoxPreference) preference).setChecked(false); + } + } + + if (name.equals(GROUP_BY_TAGS) && !dynamicTagsEnabled && groupByTags) { + preferences.edit().putBoolean(SHOW_DYNAMIC_TAGS, true).apply(); + Preference preference = mSettingsFragment.findPreference(SHOW_DYNAMIC_TAGS); + if (preference instanceof CheckBoxPreference) { + ((CheckBoxPreference) preference).setChecked(true); + } + } } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index aa340f7f5..8a1ca7b2a 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -9,6 +9,10 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.DataSetObserver; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -17,6 +21,7 @@ import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; @@ -27,6 +32,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; @@ -34,7 +40,12 @@ import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.EditText; +import android.widget.ExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.FrameLayout; +import android.widget.ListAdapter; import android.widget.ListView; +import android.widget.Space; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -65,11 +76,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; 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.function.Function; import java.util.stream.Collectors; import eu.siacs.conversations.Config; @@ -91,6 +104,7 @@ 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.StringUtils; @@ -113,16 +127,19 @@ public class StartConversationActivity extends XmppActivity implements XmppConne public int contact_context_id; private ListPagerAdapter mListPagerAdapter; private final List contacts = new ArrayList<>(); - private ListItemAdapter mContactsAdapter; + private ExpandableListItemAdapter mContactsAdapter; private TagsAdapter mTagsAdapter = new TagsAdapter(); private final List conferences = new ArrayList<>(); - private ListItemAdapter mConferenceAdapter; + private ExpandableListItemAdapter mConferenceAdapter; private final List mActivatedAccounts = new ArrayList<>(); private EditText mSearchEditText; private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false); private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); private boolean mHideOfflineContacts = false; private boolean createdByViewIntent = false; + + boolean groupingEnabled = false; + private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { @Override @@ -295,8 +312,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); binding.startConversationViewPager.setAdapter(mListPagerAdapter); - mConferenceAdapter = new ListItemAdapter(this, conferences); - mContactsAdapter = new ListItemAdapter(this, contacts); + mConferenceAdapter = new ExpandableListItemAdapter(this, conferences); + mContactsAdapter = new ExpandableListItemAdapter(this, contacts); mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener); final SharedPreferences preferences = getPreferences(); @@ -317,6 +334,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne intent = savedInstanceState.getParcelable("intent"); } + if (savedInstanceState == null) { + groupingEnabled = preferences.getBoolean(SettingsActivity.GROUP_BY_TAGS, getResources().getBoolean(R.bool.group_by_tags)); + } else { + groupingEnabled = savedInstanceState.getBoolean("groupingEnabled"); + } + if (isViewIntent(intent)) { pendingViewIntent.push(intent); createdByViewIntent = true; @@ -386,6 +409,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get()); savedInstanceState.putBoolean("opened_fab", mOpenedFab.get()); savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent); + savedInstanceState.putBoolean("groupingEnabled", groupingEnabled); if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); } @@ -1255,6 +1279,25 @@ public class StartConversationActivity extends XmppActivity implements XmppConne this.mOnItemClickListener = l; } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + + if (getActivity() instanceof StartConversationActivity && ((StartConversationActivity) getActivity()).groupingEnabled) { + FixedExpandableListView lv = new FixedExpandableListView(view.getContext()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + + ListView oldList = view.findViewById(android.R.id.list); + ViewGroup oldListParent = (ViewGroup) oldList.getParent(); + oldListParent.removeView(oldList); + oldListParent.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + + return view; + } + @Override public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -1506,4 +1549,172 @@ public class StartConversationActivity extends XmppActivity implements XmppConne notifyDataSetChanged(); } } + + static class FixedExpandableListView extends ExpandableListView { + public FixedExpandableListView(Context context) { + super(context); + } + + public FixedExpandableListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FixedExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public FixedExpandableListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setAdapter(ListAdapter adapter) { + if (adapter instanceof ExpandableListAdapter) { + setAdapter((ExpandableListAdapter) adapter); + } else { + super.setAdapter(adapter); + } + } + } + + class ExpandableListItemAdapter extends ListItemAdapter implements ExpandableListAdapter { + + private List tags = new ArrayList<>(); + private String generalTagName = activity.getString(R.string.contact_tag_general); + private ListItem.Tag generalTag = new ListItem.Tag(generalTagName, UIHelper.getColorForName(generalTagName,true)); + + private Map> groupedItems = new HashMap<>(); + + public ExpandableListItemAdapter(XmppActivity activity, List objects) { + super(activity, objects); + + registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + if (activity instanceof StartConversationActivity && ((StartConversationActivity) activity).groupingEnabled) { + tags.clear(); + tags.addAll(mTagsAdapter.tags); + + for (int i=tags.size()-1;i>=0;i--) { + if (UIHelper.isStatusTag(activity, tags.get(i))) { + tags.remove(tags.get(i)); + } + } + + groupedItems.clear(); + + boolean generalTagAdded = false; + + for (int i=0;i itemTags = item.getTags(activity); + + if (itemTags.size() == 0 || (itemTags.size() == 1 && UIHelper.isStatusTag(activity, itemTags.get(0)))) { + if (!generalTagAdded) { + tags.add(0, generalTag); + generalTagAdded = true; + } + + List group = groupedItems.computeIfAbsent(generalTag, tag -> new ArrayList<>()); + group.add(item); + } else { + for (ListItem.Tag itemTag : itemTags) { + if (UIHelper.isStatusTag(activity, itemTag)) { + continue; + } + + List group = groupedItems.computeIfAbsent(itemTag, tag -> new ArrayList<>()); + group.add(item); + } + } + } + + for (int i=tags.size()-1;i>=0;i--) { + if (groupedItems.get(tags.get(i)) == null) { + tags.remove(tags.get(i)); + } + } + } + } + }); + } + + @Override + public int getGroupCount() { + return tags.size(); + } + + @Override + public int getChildrenCount(int groupPosition) { + return groupedItems.get(tags.get(groupPosition)).size(); + } + + @Override + public Object getGroup(int groupPosition) { + return tags.get(groupPosition); + } + + @Override + public Object getChild(int groupPosition, int childPosition) { + return groupedItems.get(tags.get(groupPosition)).get(childPosition); + } + + @Override + public long getGroupId(int groupPosition) { + return tags.get(groupPosition).hashCode(); + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return groupedItems.get(tags.get(groupPosition)).get(childPosition).getJid().hashCode(); + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { + ListItem.Tag tag = tags.get(groupPosition); + + View v = activity.getLayoutInflater().inflate(R.layout.contact_group, parent, false); + + TextView tv = v.findViewById(R.id.text); + tv.setText(activity.getString(R.string.contact_tag_with_total, tag.getName(), getChildrenCount(groupPosition))); + tv.setBackgroundColor(tag.getColor()); + + return v; + } + + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { + ListItem item = groupedItems.get(tags.get(groupPosition)).get(childPosition); + return super.getView(super.getPosition(item), convertView, parent); + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return false; + } + + @Override + public void onGroupExpanded(int groupPosition) { + + } + + @Override + public void onGroupCollapsed(int groupPosition) { + + } + + @Override + public long getCombinedChildId(long groupId, long childId) { + return 0x8000000000000000L | ((groupId & 0x7FFFFFFF) << 32) | (childId & 0xFFFFFFFF); + } + + @Override + public long getCombinedGroupId(long groupId) { + return (groupId & 0x7FFFFFFF) << 32; + } + + private int dpToPx(int dp) { + return (int) (dp * Resources.getSystem().getDisplayMetrics().density); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index b70bfc558..f5d69115e 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -602,6 +602,15 @@ public class UIHelper { } } + public static boolean isStatusTag(Context context, ListItem.Tag tag) { + String name = tag.getName(); + return name.equals(context.getString(R.string.presence_chat)) || + name.equals(context.getString(R.string.presence_away)) || + name.equals(context.getString(R.string.presence_xa)) || + name.equals(context.getString(R.string.presence_dnd)) || + name.equals(context.getString(R.string.presence_online)); + } + public static String filesizeToString(long size) { if (size > (1.5 * 1024 * 1024)) { return Math.round(size * 1f / (1024 * 1024)) + " MiB"; diff --git a/src/main/res/layout/contact_group.xml b/src/main/res/layout/contact_group.xml new file mode 100644 index 000000000..50b385358 --- /dev/null +++ b/src/main/res/layout/contact_group.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index 03c6af44b..316f414dc 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -21,6 +21,7 @@ true recent false + false true 0 false diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6eaad8464..0f50b44d4 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -356,6 +356,8 @@ No app found to view contact Dynamic Tags Allow organizing with tags + Group by Dynamic Tags + Allow to grouping contacts by their tags Enable notifications No group chat server found Could not create group chat @@ -1049,4 +1051,6 @@ could_not_create_file %1$s (%2$s) %1$d selected + General + %1$s (%2$d) diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index d4b49da9d..c6a5c4a1b 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -176,6 +176,11 @@ android:key="show_dynamic_tags" android:summary="@string/pref_show_dynamic_tags_summary" android:title="@string/pref_show_dynamic_tags" /> +