diff --git a/build.gradle b/build.gradle
index 2051cd41e..c513928b6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -87,6 +87,8 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
+
+ implementation 'com.splitwise:tokenautocomplete:3.0.2'
}
ext {
diff --git a/src/conversations/res/layout/actionview_edit.xml b/src/conversations/res/layout/actionview_edit.xml
new file mode 100644
index 000000000..257dafc5b
--- /dev/null
+++ b/src/conversations/res/layout/actionview_edit.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index 37bba38cb..e889be11e 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -184,12 +184,19 @@ public class Contact implements ListItem, Blockable {
return jid;
}
- @Override
- public List getTags(Context context) {
+ public List getGroupTags() {
final ArrayList tags = new ArrayList<>();
for (final String group : getGroups(true)) {
tags.add(new Tag(group, UIHelper.getColorForName(group)));
}
+ return tags;
+ }
+
+ @Override
+ public List getTags(Context context) {
+ final HashSet tags = new HashSet<>();
+ tags.addAll(getGroupTags());
+
Presence.Status status = getShownStatus();
if (status != Presence.Status.OFFLINE) {
tags.add(UIHelper.getTagForStatus(context, status));
@@ -197,7 +204,10 @@ public class Contact implements ListItem, Blockable {
if (isBlocked()) {
tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
}
- return tags;
+ if (!showInRoster() && getSystemAccount() != null) {
+ tags.add(new Tag("Android", UIHelper.getColorForName("Android")));
+ }
+ return new ArrayList<>(tags);
}
public boolean match(Context context, String needle) {
@@ -316,6 +326,10 @@ public class Contact implements ListItem, Blockable {
return systemAccount;
}
+ public void setGroups(List groups) {
+ this.groups = new JSONArray(groups);
+ }
+
public void setSystemAccount(Uri lookupUri) {
this.systemAccount = lookupUri;
}
diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java
index fc8e9b6c6..3bb9506a0 100644
--- a/src/main/java/eu/siacs/conversations/entities/ListItem.java
+++ b/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, AvatarService.Avatarable
List getTags(Context context);
- final class Tag {
+ final class Tag implements Serializable {
private final String name;
private final int color;
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index 7c1d08643..4f67e66f0 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -2,10 +2,12 @@ package eu.siacs.conversations.ui;
import android.Manifest;
import android.content.ActivityNotFoundException;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -16,13 +18,18 @@ import android.provider.ContactsContract.Intents;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;
+import android.util.TypedValue;
+import android.view.inputmethod.InputMethodManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@@ -32,9 +39,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;
@@ -43,6 +54,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;
@@ -55,6 +67,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.GridManager;
import eu.siacs.conversations.ui.util.JidDialog;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
+import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
@@ -62,6 +75,7 @@ import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
@@ -73,6 +87,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private final int REQUEST_SYNC_CONTACTS = 0x28cf;
ActivityContactDetailsBinding binding;
private MediaAdapter mMediaAdapter;
+ protected MenuItem edit = null;
+ protected MenuItem save = null;
private Contact contact;
private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
@@ -185,7 +201,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
protected void refreshUiReal() {
- invalidateOptionsMenu();
populateView();
}
@@ -194,7 +209,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
if (http) {
return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
} else {
- return "xmpp:" + contact.getJid().asBareJid().toEscapedString();
+ return "xmpp:" + Uri.encode(contact.getJid().asBareJid().toEscapedString(), "@/+");
}
}
@@ -242,7 +257,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
recreate();
} else {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
- this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false);
+ this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags));
this.showLastSeen = preferences.getBoolean("last_activity", false);
}
binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
@@ -262,6 +277,19 @@ 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();
+ }
+ if (save != null) save.setVisible(false);
+ }
+
@Override
public boolean onOptionsItemSelected(final MenuItem menuItem) {
if (MenuDoubleTabUtil.shouldIgnoreTap()) {
@@ -285,16 +313,56 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
.setPositiveButton(getString(R.string.delete),
removeFromRoster).create().show();
break;
+ case R.id.action_save:
+ saveEdits();
+ break;
case R.id.action_edit_contact:
Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
- quickEdit(contact.getServerName(), R.string.contact_name, value -> {
- contact.setServerName(value);
- ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
- populateView();
- return null;
- }, true);
+ menuItem.expandActionView();
+ EditText text = menuItem.getActionView().findViewById(R.id.search_field);
+ text.setOnEditorActionListener((v, actionId, event) -> {
+ saveEdits();
+ return true;
+ });
+ text.setText(contact.getServerName());
+ text.requestFocus();
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(text, InputMethodManager.SHOW_IMPLICIT);
+ }
+ binding.tags.setVisibility(View.GONE);
+ binding.editTags.clearSync();
+ for (final ListItem.Tag group : contact.getGroupTags()) {
+ binding.editTags.addObjectSync(group);
+ }
+ ArrayList 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> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
+ sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
+
+ ArrayAdapter 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);
+ if (showDynamicTags) binding.editTags.setVisibility(View.VISIBLE);
+ if (save != null) save.setVisible(true);
} else {
+ menuItem.collapseActionView();
+ if (save != null) save.setVisible(false);
Intent intent = new Intent(Intent.ACTION_EDIT);
intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
intent.putExtra("finishActivityOnSaveCompleted", true);
@@ -320,6 +388,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.contact_details, menu);
AccountUtils.showHideMenuItems(menu);
+ edit = menu.findItem(R.id.action_edit_contact);
+ save = menu.findItem(R.id.action_save);
MenuItem block = menu.findItem(R.id.action_block);
MenuItem unblock = menu.findItem(R.id.action_unblock);
MenuItem edit = menu.findItem(R.id.action_edit_contact);
@@ -342,6 +412,19 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
edit.setVisible(false);
delete.setVisible(false);
}
+ edit.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ SoftKeyboardUtils.hideSoftKeyboard(ContactDetailsActivity.this);
+ binding.editTags.setVisibility(View.GONE);
+ if (save != null) save.setVisible(false);
+ populateView();
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) { return true; }
+ });
return super.onCreateOptionsMenu(menu);
}
@@ -349,6 +432,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
if (contact == null) {
return;
}
+ if (binding.editTags.getVisibility() != View.GONE) return;
invalidateOptionsMenu();
setTitle(contact.getDisplayName());
if (contact.showInRoster()) {
@@ -549,6 +633,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
}
+
populateView();
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/TagEditorView.java b/src/main/java/eu/siacs/conversations/ui/widget/TagEditorView.java
new file mode 100644
index 000000000..bc2c89918
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/widget/TagEditorView.java
@@ -0,0 +1,57 @@
+package eu.siacs.conversations.ui.widget;
+
+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 {
+ 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();
+ }
+}
diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml
index a7d569a4a..2f21193b4 100644
--- a/src/main/res/layout/activity_contact_details.xml
+++ b/src/main/res/layout/activity_contact_details.xml
@@ -68,6 +68,16 @@
android:layout_marginTop="4dp"
android:orientation="horizontal">
+
+
\ No newline at end of file
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 6b135a81a..25cf99588 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -353,7 +353,7 @@
No app found to open link
No app found to view contact
Dynamic Tags
- Display read-only tags underneath contacts
+ Allow organizing with tags
Enable notifications
No group chat server found
Could not create group chat