Allow editing tags on a contact

Stephen Paul Weber created

Change summary

build.gradle                                                        |  1 
src/cheogram/java/com/cheogram/android/TagEditorView.java           | 57 
src/main/java/eu/siacs/conversations/entities/Contact.java          | 14 
src/main/java/eu/siacs/conversations/entities/ListItem.java         |  3 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java | 36 
src/main/res/layout/activity_contact_details.xml                    |  9 
6 files changed, 117 insertions(+), 3 deletions(-)

Detailed changes

build.gradle 🔗

@@ -109,6 +109,7 @@ dependencies {
     implementation 'io.github.nishkarsh:android-permissions:2.1.6'
     implementation 'androidx.recyclerview:recyclerview:1.1.0'
     implementation 'com.github.ipld:java-cid:v1.3.1'
+    implementation 'com.splitwise:tokenautocomplete:3.0.2'
     implementation urlFile('https://gateway.pinata.cloud/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar')
     // INSERT
 }

src/cheogram/java/com/cheogram/android/TagEditorView.java 🔗

@@ -0,0 +1,57 @@
+package com.cheogram.android;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.tokenautocomplete.TokenCompleteTextView;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class TagEditorView extends TokenCompleteTextView<ListItem.Tag> {
+	public TagEditorView(Context context, AttributeSet attrs) {
+		super(context, attrs);
+		setTokenClickStyle(TokenCompleteTextView.TokenClickStyle.Delete);
+		setThreshold(1);
+		performBestGuess(false);
+		allowCollapse(false);
+	}
+
+	public void clearSync() {
+		for (ListItem.Tag tag : getObjects()) {
+			removeObjectSync(tag);
+		}
+	}
+
+	@Override
+	protected View getViewForObject(ListItem.Tag tag) {
+		LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
+		final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, (ViewGroup) getParent(), false);
+		tv.setText(tag.getName());
+		tv.setBackgroundColor(tag.getColor());
+		return tv;
+	}
+
+	@Override
+	protected ListItem.Tag defaultObject(String completionText) {
+		return new ListItem.Tag(completionText, UIHelper.getColorForName(completionText));
+	}
+
+	@Override
+	public boolean shouldIgnoreToken(ListItem.Tag tag) {
+		return getObjects().contains(tag);
+	}
+
+	@Override
+	public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
+		super.onFocusChanged(hasFocus, direction, previous);
+		performCompletion();
+	}
+}

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -189,12 +189,18 @@ public class Contact implements ListItem, Blockable {
         return jid;
     }
 
-    @Override
-    public List<Tag> getTags(Context context) {
+    public List<Tag> getGroupTags() {
         final ArrayList<Tag> tags = new ArrayList<>();
         for (final String group : getGroups(true)) {
             tags.add(new Tag(group, UIHelper.getColorForName(group)));
         }
+        return tags;
+    }
+
+    @Override
+    public List<Tag> getTags(Context context) {
+        final ArrayList<Tag> tags = new ArrayList<>();
+        tags.addAll(getGroupTags());
         for (final String tag : getSystemTags(true)) {
             tags.add(new Tag(tag, UIHelper.getColorForName(tag)));
         }
@@ -344,6 +350,10 @@ public class Contact implements ListItem, Blockable {
         this.systemAccount = lookupUri;
     }
 
+    public void setGroups(List<String> groups) {
+        this.groups = new JSONArray(groups);
+    }
+
     private Collection<String> getGroups(final boolean unique) {
         final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
         for (int i = 0; i < this.groups.length(); ++i) {

src/main/java/eu/siacs/conversations/entities/ListItem.java 🔗

@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
 
 import android.content.Context;
 
+import java.io.Serializable;
 import java.util.List;
 import java.util.Locale;
 
@@ -16,7 +17,7 @@ public interface ListItem extends Comparable<ListItem>, AvatarService.Avatarable
 
 	List<Tag> getTags(Context context);
 
-	final class Tag {
+	final class Tag implements Serializable {
 		private final String name;
 		private final int color;
 

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -21,6 +21,7 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.EditText;
@@ -33,9 +34,13 @@ import androidx.databinding.DataBindingUtil;
 
 import org.openintents.openpgp.util.OpenPgpUtils;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -44,6 +49,7 @@ import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.services.AbstractQuickConversationsService;
@@ -266,9 +272,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     }
 
     protected void saveEdits() {
+        binding.editTags.setVisibility(View.GONE);
         if (edit != null) {
             EditText text = edit.getActionView().findViewById(R.id.search_field);
             contact.setServerName(text.getText().toString());
+            contact.setGroups(binding.editTags.getObjects().stream().map(tag -> tag.getName()).collect(Collectors.toList()));
             ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
             populateView();
             edit.collapseActionView();
@@ -313,6 +321,34 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                     });
                     text.setText(contact.getServerName());
                     text.requestFocus();
+                    binding.tags.setVisibility(View.GONE);
+                    binding.editTags.clearSync();
+                    for (final ListItem.Tag group : contact.getGroupTags()) {
+                        binding.editTags.addObjectSync(group);
+                    }
+                    ArrayList<ListItem.Tag> tags = new ArrayList<>();
+                    for (final Account account : xmppConnectionService.getAccounts()) {
+                        for (Contact contact : account.getRoster().getContacts()) {
+                            tags.addAll(contact.getTags(this));
+                        }
+                        for (Bookmark bookmark : account.getBookmarks()) {
+                            tags.addAll(bookmark.getTags(this));
+                        }
+                    }
+                    Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
+                    sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
+
+                    ArrayAdapter<ListItem.Tag> adapter = new ArrayAdapter<>(
+                        this,
+                        android.R.layout.simple_list_item_1,
+                        tags.stream()
+                        .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
+                        .entrySet().stream()
+                       .sorted(sortTagsBy)
+                       .map(e -> e.getKey()).collect(Collectors.toList())
+                    );
+                    binding.editTags.setAdapter(adapter);
+                    binding.editTags.setVisibility(View.VISIBLE);
                     if (save != null) save.setVisible(true);
                 } else {
                     menuItem.collapseActionView();

src/main/res/layout/activity_contact_details.xml 🔗

@@ -67,6 +67,15 @@
                                 android:layout_marginTop="4dp"
                                 android:orientation="horizontal"></com.wefika.flowlayout.FlowLayout>
 
+                            <com.cheogram.android.TagEditorView
+                                android:id="@+id/edit_tags"
+                                android:visibility="gone"
+                                android:layout_width="match_parent"
+                                android:layout_height="wrap_content"
+                                android:layout_marginBottom="4dp"
+                                android:layout_marginLeft="-4dp"
+                                android:layout_marginTop="-4dp" />
+
                             <TextView
                                 android:id="@+id/details_lastseen"
                                 android:layout_width="wrap_content"