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"> + + + xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="collapseActionView|always" + android:title="@string/action_edit_contact" + app:actionLayout="@layout/actionview_edit" /> + + - \ 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