group contacts by tag

This commit is contained in:
kosyak 2023-10-25 04:13:36 +02:00
parent d1d23e4627
commit 43870114d9
7 changed files with 277 additions and 4 deletions

View file

@ -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 SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
public static final String OMEMO_SETTING = "omemo"; public static final String OMEMO_SETTING = "omemo";
public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
public static final String GROUP_BY_TAGS = "groupByTags";
public static final int REQUEST_CREATE_BACKUP = 0xbf8701; public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
@ -536,6 +537,25 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
if (xmppConnectionService.reconfigurePushDistributor()) { if (xmppConnectionService.reconfigurePushDistributor()) {
xmppConnectionService.renewUnifiedPushEndpoints(); 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);
}
}
} }
} }

View file

@ -9,6 +9,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; 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.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -17,6 +21,7 @@ import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.ContextMenu; import android.view.ContextMenu;
@ -27,6 +32,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
@ -34,7 +40,12 @@ import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.AutoCompleteTextView;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText; 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.ListView;
import android.widget.Space;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -65,11 +76,13 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import eu.siacs.conversations.Config; 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.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils; 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.ui.widget.SwipeRefreshListFragment;
import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.StringUtils;
@ -113,16 +127,19 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
public int contact_context_id; public int contact_context_id;
private ListPagerAdapter mListPagerAdapter; private ListPagerAdapter mListPagerAdapter;
private final List<ListItem> contacts = new ArrayList<>(); private final List<ListItem> contacts = new ArrayList<>();
private ListItemAdapter mContactsAdapter; private ExpandableListItemAdapter mContactsAdapter;
private TagsAdapter mTagsAdapter = new TagsAdapter(); private TagsAdapter mTagsAdapter = new TagsAdapter();
private final List<ListItem> conferences = new ArrayList<>(); private final List<ListItem> conferences = new ArrayList<>();
private ListItemAdapter mConferenceAdapter; private ExpandableListItemAdapter mConferenceAdapter;
private final List<String> mActivatedAccounts = new ArrayList<>(); private final List<String> mActivatedAccounts = new ArrayList<>();
private EditText mSearchEditText; private EditText mSearchEditText;
private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false); private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
private boolean mHideOfflineContacts = false; private boolean mHideOfflineContacts = false;
private boolean createdByViewIntent = false; private boolean createdByViewIntent = false;
boolean groupingEnabled = false;
private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
@Override @Override
@ -295,8 +312,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
binding.startConversationViewPager.setAdapter(mListPagerAdapter); binding.startConversationViewPager.setAdapter(mListPagerAdapter);
mConferenceAdapter = new ListItemAdapter(this, conferences); mConferenceAdapter = new ExpandableListItemAdapter(this, conferences);
mContactsAdapter = new ListItemAdapter(this, contacts); mContactsAdapter = new ExpandableListItemAdapter(this, contacts);
mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener); mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
final SharedPreferences preferences = getPreferences(); final SharedPreferences preferences = getPreferences();
@ -317,6 +334,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
intent = savedInstanceState.getParcelable("intent"); 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)) { if (isViewIntent(intent)) {
pendingViewIntent.push(intent); pendingViewIntent.push(intent);
createdByViewIntent = true; createdByViewIntent = true;
@ -386,6 +409,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get()); savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get());
savedInstanceState.putBoolean("opened_fab", mOpenedFab.get()); savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent); savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
savedInstanceState.putBoolean("groupingEnabled", groupingEnabled);
if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
} }
@ -1255,6 +1279,25 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
this.mOnItemClickListener = l; 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 @Override
public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
@ -1506,4 +1549,172 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
notifyDataSetChanged(); 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<ListItem.Tag> 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<ListItem.Tag, List<ListItem>> groupedItems = new HashMap<>();
public ExpandableListItemAdapter(XmppActivity activity, List<ListItem> 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<ExpandableListItemAdapter.super.getCount();i++) {
ListItem item = getItem(i);
List<ListItem.Tag> 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<ListItem> group = groupedItems.computeIfAbsent(generalTag, tag -> new ArrayList<>());
group.add(item);
} else {
for (ListItem.Tag itemTag : itemTags) {
if (UIHelper.isStatusTag(activity, itemTag)) {
continue;
}
List<ListItem> 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);
}
}
} }

View file

@ -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) { public static String filesizeToString(long size) {
if (size > (1.5 * 1024 * 1024)) { if (size > (1.5 * 1024 * 1024)) {
return Math.round(size * 1f / (1024 * 1024)) + " MiB"; return Math.round(size * 1f / (1024 * 1024)) + " MiB";

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:textAllCaps="true"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_marginStart="40dp"
android:layout_marginEnd="16dp" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?color_background_tertiary" />
</FrameLayout>

View file

@ -21,6 +21,7 @@
<bool name="use_green_background">true</bool> <bool name="use_green_background">true</bool>
<string name="quick_action">recent</string> <string name="quick_action">recent</string>
<bool name="show_dynamic_tags">false</bool> <bool name="show_dynamic_tags">false</bool>
<bool name="group_by_tags">false</bool>
<bool name="btbv">true</bool> <bool name="btbv">true</bool>
<integer name="automatic_message_deletion">0</integer> <integer name="automatic_message_deletion">0</integer>
<bool name="dont_trust_system_cas">false</bool> <bool name="dont_trust_system_cas">false</bool>

View file

@ -356,6 +356,8 @@
<string name="no_application_found_to_view_contact">No app found to view contact</string> <string name="no_application_found_to_view_contact">No app found to view contact</string>
<string name="pref_show_dynamic_tags">Dynamic Tags</string> <string name="pref_show_dynamic_tags">Dynamic Tags</string>
<string name="pref_show_dynamic_tags_summary">Allow organizing with tags</string> <string name="pref_show_dynamic_tags_summary">Allow organizing with tags</string>
<string name="pref_group_by_tags">Group by Dynamic Tags</string>
<string name="pref_group_by_tags_summary">Allow to grouping contacts by their tags</string>
<string name="enable_notifications">Enable notifications</string> <string name="enable_notifications">Enable notifications</string>
<string name="no_conference_server_found">No group chat server found</string> <string name="no_conference_server_found">No group chat server found</string>
<string name="conference_creation_failed">Could not create group chat</string> <string name="conference_creation_failed">Could not create group chat</string>
@ -1049,4 +1051,6 @@
<string name="could_not_create_file">could_not_create_file</string> <string name="could_not_create_file">could_not_create_file</string>
<string name="muc_private_conversation_title">%1$s (%2$s)</string> <string name="muc_private_conversation_title">%1$s (%2$s)</string>
<string name="message_selection_title">%1$d selected</string> <string name="message_selection_title">%1$d selected</string>
<string name="contact_tag_general">General</string>
<string name="contact_tag_with_total">%1$s (%2$d)</string>
</resources> </resources>

View file

@ -176,6 +176,11 @@
android:key="show_dynamic_tags" android:key="show_dynamic_tags"
android:summary="@string/pref_show_dynamic_tags_summary" android:summary="@string/pref_show_dynamic_tags_summary"
android:title="@string/pref_show_dynamic_tags" /> android:title="@string/pref_show_dynamic_tags" />
<CheckBoxPreference
android:defaultValue="@bool/group_by_tags"
android:key="groupByTags"
android:summary="@string/pref_group_by_tags_summary"
android:title="@string/pref_group_by_tags" />
<ListPreference <ListPreference
android:defaultValue="@string/theme" android:defaultValue="@string/theme"
android:entries="@array/themes" android:entries="@array/themes"