* Add hashtag autocompletion, closes #769 * Apply review feedback
This commit is contained in:
parent
96162ab544
commit
a3ee13d767
|
@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
@ -39,25 +38,6 @@ import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.Px;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import androidx.transition.TransitionManager;
|
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputFilter;
|
import android.text.InputFilter;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
@ -81,10 +61,12 @@ import android.widget.PopupMenu;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.keylesspalace.tusky.adapter.EmojiAdapter;
|
import com.keylesspalace.tusky.adapter.EmojiAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
|
import com.keylesspalace.tusky.adapter.MentionTagAutoCompleteAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener;
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AppDatabase;
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
|
@ -94,6 +76,7 @@ import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.Instance;
|
import com.keylesspalace.tusky.entity.Instance;
|
||||||
|
import com.keylesspalace.tusky.entity.SearchResults;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody;
|
import com.keylesspalace.tusky.network.ProgressRequestBody;
|
||||||
|
@ -101,7 +84,7 @@ import com.keylesspalace.tusky.service.SendTootService;
|
||||||
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
||||||
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
||||||
import com.keylesspalace.tusky.util.ListUtils;
|
import com.keylesspalace.tusky.util.ListUtils;
|
||||||
import com.keylesspalace.tusky.util.MentionTokenizer;
|
import com.keylesspalace.tusky.util.MentionTagTokenizer;
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||||
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
|
@ -131,12 +114,31 @@ import java.util.Locale;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.Px;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||||
|
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.SingleObserver;
|
import io.reactivex.SingleObserver;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import kotlin.collections.CollectionsKt;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
@ -155,7 +157,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
|
||||||
public final class ComposeActivity
|
public final class ComposeActivity
|
||||||
extends BaseActivity
|
extends BaseActivity
|
||||||
implements ComposeOptionsListener,
|
implements ComposeOptionsListener,
|
||||||
MentionAutoCompleteAdapter.AccountSearchProvider,
|
MentionTagAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable, InputConnectionCompat.OnCommitContentListener {
|
Injectable, InputConnectionCompat.OnCommitContentListener {
|
||||||
|
|
||||||
|
@ -225,7 +227,8 @@ public final class ComposeActivity
|
||||||
private int savedTootUid = 0;
|
private int savedTootUid = 0;
|
||||||
private List<Emoji> emojiList;
|
private List<Emoji> emojiList;
|
||||||
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
|
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
|
||||||
private @Px int thumbnailViewSize;
|
private @Px
|
||||||
|
int thumbnailViewSize;
|
||||||
|
|
||||||
private SaveTootHelper saveTootHelper;
|
private SaveTootHelper saveTootHelper;
|
||||||
private Gson gson = new Gson();
|
private Gson gson = new Gson();
|
||||||
|
@ -516,8 +519,8 @@ public final class ComposeActivity
|
||||||
});
|
});
|
||||||
|
|
||||||
textEditor.setAdapter(
|
textEditor.setAdapter(
|
||||||
new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete, this));
|
new MentionTagAutoCompleteAdapter(this));
|
||||||
textEditor.setTokenizer(new MentionTokenizer());
|
textEditor.setTokenizer(new MentionTagTokenizer());
|
||||||
|
|
||||||
// Add any mentions to the text field when a reply is first composed.
|
// Add any mentions to the text field when a reply is first composed.
|
||||||
if (mentionedUsernames != null) {
|
if (mentionedUsernames != null) {
|
||||||
|
@ -983,7 +986,7 @@ public final class ComposeActivity
|
||||||
spoilerText = contentWarningEditor.getText().toString();
|
spoilerText = contentWarningEditor.getText().toString();
|
||||||
}
|
}
|
||||||
int characterCount = calculateTextLength();
|
int characterCount = calculateTextLength();
|
||||||
if (characterCount <= 0 && mediaQueued.size()==0) {
|
if (characterCount <= 0 && mediaQueued.size() == 0) {
|
||||||
textEditor.setError(getString(R.string.error_empty));
|
textEditor.setError(getString(R.string.error_empty));
|
||||||
enableButtons();
|
enableButtons();
|
||||||
} else if (characterCount <= maximumTootCharacters) {
|
} else if (characterCount <= maximumTootCharacters) {
|
||||||
|
@ -1532,19 +1535,38 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Account> searchAccounts(String mention) {
|
public List<MentionTagAutoCompleteAdapter.AutocompleteResult> search(String token) {
|
||||||
ArrayList<Account> resultList = new ArrayList<>();
|
|
||||||
try {
|
try {
|
||||||
List<Account> accountList = mastodonApi.searchAccounts(mention, false, 40)
|
switch (token.charAt(0)) {
|
||||||
.execute()
|
case '@':
|
||||||
.body();
|
ArrayList<Account> resultList = new ArrayList<>();
|
||||||
if (accountList != null) {
|
List<Account> accountList = mastodonApi
|
||||||
resultList.addAll(accountList);
|
.searchAccounts(token.substring(1), false, 20)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
if (accountList != null) {
|
||||||
|
resultList.addAll(accountList);
|
||||||
|
}
|
||||||
|
return CollectionsKt.map(resultList, MentionTagAutoCompleteAdapter.AccountResult::new);
|
||||||
|
case '#':
|
||||||
|
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
return CollectionsKt.map(
|
||||||
|
response.body().getHashtags(),
|
||||||
|
MentionTagAutoCompleteAdapter.HashtagResult::new
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
Log.w(TAG, "Unexpected autocompletion token: " + token);
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", mention));
|
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return resultList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.LayoutRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Filter;
|
|
||||||
import android.widget.Filterable;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by charlag on 12/11/17.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class MentionAutoCompleteAdapter extends ArrayAdapter<Account>
|
|
||||||
implements Filterable {
|
|
||||||
private ArrayList<Account> resultList;
|
|
||||||
@LayoutRes
|
|
||||||
private int layoutId;
|
|
||||||
private final AccountSearchProvider accountSearchProvider;
|
|
||||||
|
|
||||||
public MentionAutoCompleteAdapter(Context context, @LayoutRes int resource,
|
|
||||||
AccountSearchProvider accountSearchProvider) {
|
|
||||||
super(context, resource);
|
|
||||||
layoutId = resource;
|
|
||||||
resultList = new ArrayList<>();
|
|
||||||
this.accountSearchProvider = accountSearchProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return resultList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Account getItem(int index) {
|
|
||||||
return resultList.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Filter getFilter() {
|
|
||||||
return new Filter() {
|
|
||||||
@Override
|
|
||||||
public CharSequence convertResultToString(Object resultValue) {
|
|
||||||
return ((Account) resultValue).getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is invoked in a worker thread.
|
|
||||||
@Override
|
|
||||||
protected FilterResults performFiltering(CharSequence constraint) {
|
|
||||||
FilterResults filterResults = new FilterResults();
|
|
||||||
if (constraint != null) {
|
|
||||||
List<Account> accounts =
|
|
||||||
accountSearchProvider.searchAccounts(constraint.toString());
|
|
||||||
filterResults.values = accounts;
|
|
||||||
filterResults.count = accounts.size();
|
|
||||||
}
|
|
||||||
return filterResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
|
||||||
if (results != null && results.count > 0) {
|
|
||||||
resultList.clear();
|
|
||||||
ArrayList<Account> newResults = (ArrayList<Account>) results.values;
|
|
||||||
resultList.addAll(newResults);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
} else {
|
|
||||||
notifyDataSetInvalidated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
|
||||||
View view = convertView;
|
|
||||||
|
|
||||||
Context context = getContext();
|
|
||||||
|
|
||||||
if (convertView == null) {
|
|
||||||
LayoutInflater layoutInflater =
|
|
||||||
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
view = layoutInflater.inflate(layoutId, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Account account = getItem(position);
|
|
||||||
if (account != null) {
|
|
||||||
TextView username = view.findViewById(R.id.username);
|
|
||||||
TextView displayName = view.findViewById(R.id.display_name);
|
|
||||||
ImageView avatar = view.findViewById(R.id.avatar);
|
|
||||||
String format = getContext().getString(R.string.status_username_format);
|
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
|
||||||
username.setText(formattedUsername);
|
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), account.getEmojis(), displayName);
|
|
||||||
displayName.setText(emojifiedName);
|
|
||||||
if (!account.getAvatar().isEmpty()) {
|
|
||||||
Picasso.with(context)
|
|
||||||
.load(account.getAvatar())
|
|
||||||
.placeholder(R.drawable.avatar_default)
|
|
||||||
.into(avatar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AccountSearchProvider {
|
|
||||||
List<Account> searchAccounts(String mention);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.Filter;
|
||||||
|
import android.widget.Filterable;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by charlag on 12/11/17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
|
implements Filterable {
|
||||||
|
private static final int ACCOUNT_VIEW_TYPE = 0;
|
||||||
|
private static final int HASHTAG_VIEW_TYPE = 1;
|
||||||
|
|
||||||
|
private final ArrayList<AutocompleteResult> resultList;
|
||||||
|
private final AutocompletionProvider autocompletionProvider;
|
||||||
|
|
||||||
|
public MentionTagAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) {
|
||||||
|
super();
|
||||||
|
resultList = new ArrayList<>();
|
||||||
|
this.autocompletionProvider = autocompletionProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return resultList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AutocompleteResult getItem(int index) {
|
||||||
|
return resultList.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Filter getFilter() {
|
||||||
|
return new Filter() {
|
||||||
|
@Override
|
||||||
|
public CharSequence convertResultToString(Object resultValue) {
|
||||||
|
if (resultValue instanceof AccountResult) {
|
||||||
|
return ((AccountResult) resultValue).account.getUsername();
|
||||||
|
} else {
|
||||||
|
return formatHashtag((HashtagResult) resultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is invoked in a worker thread.
|
||||||
|
@Override
|
||||||
|
protected FilterResults performFiltering(CharSequence constraint) {
|
||||||
|
FilterResults filterResults = new FilterResults();
|
||||||
|
if (constraint != null) {
|
||||||
|
List<AutocompleteResult> results =
|
||||||
|
autocompletionProvider.search(constraint.toString());
|
||||||
|
filterResults.values = results;
|
||||||
|
filterResults.count = results.size();
|
||||||
|
}
|
||||||
|
return filterResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
|
if (results != null && results.count > 0) {
|
||||||
|
resultList.clear();
|
||||||
|
resultList.addAll((List<AutocompleteResult>) results.values);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
} else {
|
||||||
|
notifyDataSetInvalidated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||||
|
View view = convertView;
|
||||||
|
final Context context = parent.getContext();
|
||||||
|
|
||||||
|
switch (getItemViewType(position)) {
|
||||||
|
case ACCOUNT_VIEW_TYPE:
|
||||||
|
AccountViewHolder holder;
|
||||||
|
if (convertView == null) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
view = ((LayoutInflater) context
|
||||||
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
|
.inflate(R.layout.item_autocomplete_account, parent, false);
|
||||||
|
}
|
||||||
|
if (view.getTag() == null) {
|
||||||
|
view.setTag(new AccountViewHolder(view));
|
||||||
|
}
|
||||||
|
holder = (AccountViewHolder) view.getTag();
|
||||||
|
|
||||||
|
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||||
|
if (accountResult != null) {
|
||||||
|
Account account = accountResult.account;
|
||||||
|
String format = context.getString(R.string.status_username_format);
|
||||||
|
String formattedUsername = String.format(format, account.getUsername());
|
||||||
|
holder.username.setText(formattedUsername);
|
||||||
|
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(),
|
||||||
|
account.getEmojis(), holder.displayName);
|
||||||
|
holder.displayName.setText(emojifiedName);
|
||||||
|
if (!account.getAvatar().isEmpty()) {
|
||||||
|
Picasso.with(context)
|
||||||
|
.load(account.getAvatar())
|
||||||
|
.placeholder(R.drawable.avatar_default)
|
||||||
|
.into(holder.avatar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HASHTAG_VIEW_TYPE:
|
||||||
|
if (convertView == null) {
|
||||||
|
view = ((LayoutInflater) context
|
||||||
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
|
.inflate(R.layout.item_hashtag, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashtagResult result = (HashtagResult) getItem(position);
|
||||||
|
if (result != null) {
|
||||||
|
((TextView) view).setText(formatHashtag(result));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("unknown view type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHashtag(HashtagResult result) {
|
||||||
|
return String.format("#%s", result.hashtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViewTypeCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
if (getItem(position) instanceof AccountResult) {
|
||||||
|
return ACCOUNT_VIEW_TYPE;
|
||||||
|
} else {
|
||||||
|
return HASHTAG_VIEW_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract static class AutocompleteResult {
|
||||||
|
AutocompleteResult() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class AccountResult extends AutocompleteResult {
|
||||||
|
private final Account account;
|
||||||
|
|
||||||
|
public AccountResult(Account account) {
|
||||||
|
this.account = account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class HashtagResult extends AutocompleteResult {
|
||||||
|
private final String hashtag;
|
||||||
|
|
||||||
|
public HashtagResult(String hashtag) {
|
||||||
|
this.hashtag = hashtag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AutocompletionProvider {
|
||||||
|
List<AutocompleteResult> search(String mention);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AccountViewHolder {
|
||||||
|
final TextView username;
|
||||||
|
final TextView displayName;
|
||||||
|
final ImageView avatar;
|
||||||
|
|
||||||
|
private AccountViewHolder(View view) {
|
||||||
|
username = view.findViewById(R.id.username);
|
||||||
|
displayName = view.findViewById(R.id.display_name);
|
||||||
|
avatar = view.findViewById(R.id.avatar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,14 +20,14 @@ import android.text.Spanned
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.widget.MultiAutoCompleteTextView
|
import android.widget.MultiAutoCompleteTextView
|
||||||
|
|
||||||
class MentionTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
class MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||||
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
|
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
|
||||||
if (cursor == 0) {
|
if (cursor == 0) {
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
var i = cursor
|
var i = cursor
|
||||||
var character = text[i - 1]
|
var character = text[i - 1]
|
||||||
while (i > 0 && character != '@') {
|
while (i > 0 && character != '@' && character != '#') {
|
||||||
// See SpanUtils.MENTION_REGEX
|
// See SpanUtils.MENTION_REGEX
|
||||||
if (!Character.isLetterOrDigit(character) && character != '_') {
|
if (!Character.isLetterOrDigit(character) && character != '_') {
|
||||||
return cursor
|
return cursor
|
||||||
|
@ -35,10 +35,12 @@ class MentionTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||||
i--
|
i--
|
||||||
character = if (i == 0) ' ' else text[i - 1]
|
character = if (i == 0) ' ' else text[i - 1]
|
||||||
}
|
}
|
||||||
if (i < 1 || character != '@') {
|
if (i < 1
|
||||||
|
|| (character != '@' && character != '#')
|
||||||
|
|| i > 1 && !Character.isWhitespace(text[i - 2])) {
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
return i
|
return i - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findTokenEnd(text: CharSequence, cursor: Int): Int {
|
override fun findTokenEnd(text: CharSequence, cursor: Int): Int {
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* Copyright 2018 Levi Bard
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.util.MentionTagTokenizer
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class MentionTagTokenizerTest(private val text: CharSequence,
|
||||||
|
private val expectedStartIndex: Int,
|
||||||
|
private val expectedEndIndex: Int) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Parameterized.Parameters(name = "{0}")
|
||||||
|
@JvmStatic
|
||||||
|
fun data(): Iterable<Any> {
|
||||||
|
return listOf(
|
||||||
|
arrayOf("@mention", 0, 8),
|
||||||
|
arrayOf("@ment10n", 0, 8),
|
||||||
|
arrayOf("@ment10n_", 0, 9),
|
||||||
|
arrayOf("@ment10n_n", 0, 10),
|
||||||
|
arrayOf("@ment10n_9", 0, 10),
|
||||||
|
arrayOf(" @mention", 1, 9),
|
||||||
|
arrayOf(" @ment10n", 1, 9),
|
||||||
|
arrayOf(" @ment10n_", 1, 10),
|
||||||
|
arrayOf(" @ment10n_ @", 11, 12),
|
||||||
|
arrayOf(" @ment10n_ @ment20n", 11, 19),
|
||||||
|
arrayOf(" @ment10n_ @ment20n_", 11, 20),
|
||||||
|
arrayOf(" @ment10n_ @ment20n_n", 11, 21),
|
||||||
|
arrayOf(" @ment10n_ @ment20n_9", 11, 21),
|
||||||
|
arrayOf("mention", 7, 7),
|
||||||
|
arrayOf("ment10n", 7, 7),
|
||||||
|
arrayOf("mentio_", 7, 7),
|
||||||
|
arrayOf("#tusky", 0, 6),
|
||||||
|
arrayOf("#@tusky", 7, 7),
|
||||||
|
arrayOf("@#tusky", 7, 7),
|
||||||
|
arrayOf(" @#tusky", 8, 8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tokenizer = MentionTagTokenizer()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tokenIndices_matchExpectations() {
|
||||||
|
Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
|
||||||
|
Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,61 +0,0 @@
|
||||||
/* Copyright 2018 Levi Bard
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.MentionTokenizer
|
|
||||||
import org.junit.Assert
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.Parameterized
|
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
|
||||||
class MentionTokenizerTest(private val text: CharSequence,
|
|
||||||
private val expectedStartIndex: Int,
|
|
||||||
private val expectedEndIndex: Int) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Parameterized.Parameters(name = "{0}")
|
|
||||||
@JvmStatic
|
|
||||||
fun data(): Iterable<Any> {
|
|
||||||
return listOf(
|
|
||||||
arrayOf("@mention", 1, 8),
|
|
||||||
arrayOf("@ment10n", 1, 8),
|
|
||||||
arrayOf("@ment10n_", 1, 9),
|
|
||||||
arrayOf("@ment10n_n", 1, 10),
|
|
||||||
arrayOf("@ment10n_9", 1, 10),
|
|
||||||
arrayOf(" @mention", 2, 9),
|
|
||||||
arrayOf(" @ment10n", 2, 9),
|
|
||||||
arrayOf(" @ment10n_", 2, 10),
|
|
||||||
arrayOf(" @ment10n_ @", 12, 12),
|
|
||||||
arrayOf(" @ment10n_ @ment20n", 12, 19),
|
|
||||||
arrayOf(" @ment10n_ @ment20n_", 12, 20),
|
|
||||||
arrayOf(" @ment10n_ @ment20n_n", 12, 21),
|
|
||||||
arrayOf(" @ment10n_ @ment20n_9", 12, 21),
|
|
||||||
arrayOf("mention", 7, 7),
|
|
||||||
arrayOf("ment10n", 7, 7),
|
|
||||||
arrayOf("mentio_", 7, 7)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val tokenizer = MentionTokenizer()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun tokenIndices_matchExpectations() {
|
|
||||||
Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
|
|
||||||
Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue