tags editor

This commit is contained in:
kosyak 2023-08-15 01:22:56 +02:00
parent dca3e80bad
commit fee1a67332
9 changed files with 213 additions and 18 deletions

View file

@ -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 {

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:focusable="true"
android:gravity="center">
<EditText
android:id="@+id/search_field"
style="@style/Widget.Conversations.SearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:imeOptions="flagNoExtractUi|actionDone"
android:inputType="textPersonName"/>
</RelativeLayout>

View file

@ -184,12 +184,19 @@ 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 HashSet<Tag> 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<String> groups) {
this.groups = new JSONArray(groups);
}
public void setSystemAccount(Uri lookupUri) {
this.systemAccount = lookupUri;
}

View file

@ -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;

View file

@ -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<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);
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();
}
}

View file

@ -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<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();
}
}

View file

@ -68,6 +68,16 @@
android:layout_marginTop="4dp"
android:orientation="horizontal"></com.wefika.flowlayout.FlowLayout>
<eu.siacs.conversations.ui.widget.TagEditorView
android:id="@+id/edit_tags"
android:visibility="gone"
android:hint="Tags"
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"

View file

@ -1,13 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_edit_contact"
android:icon="?attr/icon_edit"
android:orderInCategory="10"
app:showAsAction="always"
android:title="@string/action_edit_contact"/>
app:showAsAction="collapseActionView|always"
android:title="@string/action_edit_contact"
app:actionLayout="@layout/actionview_edit" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save_white_24dp"
android:title="@string/save"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/action_share"

View file

@ -353,7 +353,7 @@
<string name="no_application_found_to_open_link">No app found to open link</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_summary">Display read-only tags underneath contacts</string>
<string name="pref_show_dynamic_tags_summary">Allow organizing with tags</string>
<string name="enable_notifications">Enable notifications</string>
<string name="no_conference_server_found">No group chat server found</string>
<string name="conference_creation_failed">Could not create group chat</string>