* Add hashtag autocompletion, closes #769 * Apply review feedback
This commit is contained in:
parent
96162ab544
commit
a3ee13d767
7 changed files with 352 additions and 244 deletions
|
@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
|
|||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -39,25 +38,6 @@ import android.os.Parcel;
|
|||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
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.InputFilter;
|
||||
import android.text.InputType;
|
||||
|
@ -81,10 +61,12 @@ import android.widget.PopupMenu;
|
|||
import android.widget.TextView;
|
||||
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.reflect.TypeToken;
|
||||
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.db.AccountEntity;
|
||||
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.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Instance;
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
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.DownsizeImageTask;
|
||||
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.SpanUtilsKt;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
|
@ -131,12 +114,31 @@ import java.util.Locale;
|
|||
|
||||
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 io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
|
@ -155,7 +157,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
|
|||
public final class ComposeActivity
|
||||
extends BaseActivity
|
||||
implements ComposeOptionsListener,
|
||||
MentionAutoCompleteAdapter.AccountSearchProvider,
|
||||
MentionTagAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable, InputConnectionCompat.OnCommitContentListener {
|
||||
|
||||
|
@ -225,7 +227,8 @@ public final class ComposeActivity
|
|||
private int savedTootUid = 0;
|
||||
private List<Emoji> emojiList;
|
||||
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
|
||||
private @Px int thumbnailViewSize;
|
||||
private @Px
|
||||
int thumbnailViewSize;
|
||||
|
||||
private SaveTootHelper saveTootHelper;
|
||||
private Gson gson = new Gson();
|
||||
|
@ -516,8 +519,8 @@ public final class ComposeActivity
|
|||
});
|
||||
|
||||
textEditor.setAdapter(
|
||||
new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete, this));
|
||||
textEditor.setTokenizer(new MentionTokenizer());
|
||||
new MentionTagAutoCompleteAdapter(this));
|
||||
textEditor.setTokenizer(new MentionTagTokenizer());
|
||||
|
||||
// Add any mentions to the text field when a reply is first composed.
|
||||
if (mentionedUsernames != null) {
|
||||
|
@ -983,7 +986,7 @@ public final class ComposeActivity
|
|||
spoilerText = contentWarningEditor.getText().toString();
|
||||
}
|
||||
int characterCount = calculateTextLength();
|
||||
if (characterCount <= 0 && mediaQueued.size()==0) {
|
||||
if (characterCount <= 0 && mediaQueued.size() == 0) {
|
||||
textEditor.setError(getString(R.string.error_empty));
|
||||
enableButtons();
|
||||
} else if (characterCount <= maximumTootCharacters) {
|
||||
|
@ -1532,19 +1535,38 @@ public final class ComposeActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<Account> searchAccounts(String mention) {
|
||||
ArrayList<Account> resultList = new ArrayList<>();
|
||||
public List<MentionTagAutoCompleteAdapter.AutocompleteResult> search(String token) {
|
||||
try {
|
||||
List<Account> accountList = mastodonApi.searchAccounts(mention, false, 40)
|
||||
.execute()
|
||||
.body();
|
||||
if (accountList != null) {
|
||||
resultList.addAll(accountList);
|
||||
switch (token.charAt(0)) {
|
||||
case '@':
|
||||
ArrayList<Account> resultList = new ArrayList<>();
|
||||
List<Account> accountList = mastodonApi
|
||||
.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) {
|
||||
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
|
||||
|
|
|
@ -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.widget.MultiAutoCompleteTextView
|
||||
|
||||
class MentionTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||
class MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
|
||||
if (cursor == 0) {
|
||||
return cursor
|
||||
}
|
||||
var i = cursor
|
||||
var character = text[i - 1]
|
||||
while (i > 0 && character != '@') {
|
||||
while (i > 0 && character != '@' && character != '#') {
|
||||
// See SpanUtils.MENTION_REGEX
|
||||
if (!Character.isLetterOrDigit(character) && character != '_') {
|
||||
return cursor
|
||||
|
@ -35,10 +35,12 @@ class MentionTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
|||
i--
|
||||
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 i
|
||||
return i - 1
|
||||
}
|
||||
|
||||
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