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 'com.github.bumptech.glide:glide:4.15.1'
implementation 'info.androidhive:imagefilters:1.0.7' implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.splitwise:tokenautocomplete:3.0.2'
} }
ext { 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; return jid;
} }
@Override public List<Tag> getGroupTags() {
public List<Tag> getTags(Context context) {
final ArrayList<Tag> tags = new ArrayList<>(); final ArrayList<Tag> tags = new ArrayList<>();
for (final String group : getGroups(true)) { for (final String group : getGroups(true)) {
tags.add(new Tag(group, UIHelper.getColorForName(group))); 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(); Presence.Status status = getShownStatus();
if (status != Presence.Status.OFFLINE) { if (status != Presence.Status.OFFLINE) {
tags.add(UIHelper.getTagForStatus(context, status)); tags.add(UIHelper.getTagForStatus(context, status));
@ -197,7 +204,10 @@ public class Contact implements ListItem, Blockable {
if (isBlocked()) { if (isBlocked()) {
tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b)); 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) { public boolean match(Context context, String needle) {
@ -316,6 +326,10 @@ public class Contact implements ListItem, Blockable {
return systemAccount; return systemAccount;
} }
public void setGroups(List<String> groups) {
this.groups = new JSONArray(groups);
}
public void setSystemAccount(Uri lookupUri) { public void setSystemAccount(Uri lookupUri) {
this.systemAccount = lookupUri; this.systemAccount = lookupUri;
} }

View file

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

View file

@ -2,10 +2,12 @@ package eu.siacs.conversations.ui;
import android.Manifest; import android.Manifest;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
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.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -16,13 +18,18 @@ import android.provider.ContactsContract.Intents;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.util.TypedValue;
import android.view.inputmethod.InputMethodManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton; import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -32,9 +39,13 @@ import androidx.databinding.DataBindingUtil;
import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; 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.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.databinding.ActivityContactDetailsBinding; import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.services.AbstractQuickConversationsService; 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.GridManager;
import eu.siacs.conversations.ui.util.JidDialog; 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.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons; 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.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
@ -73,6 +87,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private final int REQUEST_SYNC_CONTACTS = 0x28cf; private final int REQUEST_SYNC_CONTACTS = 0x28cf;
ActivityContactDetailsBinding binding; ActivityContactDetailsBinding binding;
private MediaAdapter mMediaAdapter; private MediaAdapter mMediaAdapter;
protected MenuItem edit = null;
protected MenuItem save = null;
private Contact contact; private Contact contact;
private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
@ -185,7 +201,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override @Override
protected void refreshUiReal() { protected void refreshUiReal() {
invalidateOptionsMenu();
populateView(); populateView();
} }
@ -194,7 +209,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
if (http) { if (http) {
return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString()); return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
} else { } 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(); recreate();
} else { } else {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); 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); this.showLastSeen = preferences.getBoolean("last_activity", false);
} }
binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); 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 @Override
public boolean onOptionsItemSelected(final MenuItem menuItem) { public boolean onOptionsItemSelected(final MenuItem menuItem) {
if (MenuDoubleTabUtil.shouldIgnoreTap()) { if (MenuDoubleTabUtil.shouldIgnoreTap()) {
@ -285,16 +313,56 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
.setPositiveButton(getString(R.string.delete), .setPositiveButton(getString(R.string.delete),
removeFromRoster).create().show(); removeFromRoster).create().show();
break; break;
case R.id.action_save:
saveEdits();
break;
case R.id.action_edit_contact: case R.id.action_edit_contact:
Uri systemAccount = contact.getSystemAccount(); Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) { if (systemAccount == null) {
quickEdit(contact.getServerName(), R.string.contact_name, value -> { menuItem.expandActionView();
contact.setServerName(value); EditText text = menuItem.getActionView().findViewById(R.id.search_field);
ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact); text.setOnEditorActionListener((v, actionId, event) -> {
populateView(); saveEdits();
return null; return true;
}, 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 { } else {
menuItem.collapseActionView();
if (save != null) save.setVisible(false);
Intent intent = new Intent(Intent.ACTION_EDIT); Intent intent = new Intent(Intent.ACTION_EDIT);
intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE); intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
intent.putExtra("finishActivityOnSaveCompleted", true); intent.putExtra("finishActivityOnSaveCompleted", true);
@ -320,6 +388,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
public boolean onCreateOptionsMenu(final Menu menu) { public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.contact_details, menu); getMenuInflater().inflate(R.menu.contact_details, menu);
AccountUtils.showHideMenuItems(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 block = menu.findItem(R.id.action_block);
MenuItem unblock = menu.findItem(R.id.action_unblock); MenuItem unblock = menu.findItem(R.id.action_unblock);
MenuItem edit = menu.findItem(R.id.action_edit_contact); MenuItem edit = menu.findItem(R.id.action_edit_contact);
@ -342,6 +412,19 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
edit.setVisible(false); edit.setVisible(false);
delete.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); return super.onCreateOptionsMenu(menu);
} }
@ -349,6 +432,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
if (contact == null) { if (contact == null) {
return; return;
} }
if (binding.editTags.getVisibility() != View.GONE) return;
invalidateOptionsMenu(); invalidateOptionsMenu();
setTitle(contact.getDisplayName()); setTitle(contact.getDisplayName());
if (contact.showInRoster()) { if (contact.showInRoster()) {
@ -549,6 +633,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this); xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact)); this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
} }
populateView(); 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:layout_marginTop="4dp"
android:orientation="horizontal"></com.wefika.flowlayout.FlowLayout> 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 <TextView
android:id="@+id/details_lastseen" android:id="@+id/details_lastseen"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -6,8 +6,16 @@
android:id="@+id/action_edit_contact" android:id="@+id/action_edit_contact"
android:icon="?attr/icon_edit" android:icon="?attr/icon_edit"
android:orderInCategory="10" android:orderInCategory="10"
app:showAsAction="always" app:showAsAction="collapseActionView|always"
android:title="@string/action_edit_contact"/> 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 <item
android:id="@+id/action_share" 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_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="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">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="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>