Add autocompletion for custom emoji (#1089)
* Remove unnecessary //noinspection ConstantConditions * Add autocompletion for custom emoji * Update MentionTagTokenizer tests for emoji autocomplete support * Move 1) emoji list retrieval notifying and 2) setting of emojiList field into setEmojiList() method of ComposeActivity * Convert RelativeLayout in item_autocomplete_emoji.xml to LinearLayout * Rename MentionTag* to Compose* * Improve emoji autocomplete matching * Make hashtag autocomplete results bold * Use Context.getString()'s built-in formatting * Add a divider between emoji autocomplete results that *start with* the token and those that *contain* it
This commit is contained in:
parent
d43b4fef4b
commit
10fcee4798
14 changed files with 232 additions and 38 deletions
|
@ -66,7 +66,7 @@ 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.MentionTagAutoCompleteAdapter;
|
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter;
|
||||||
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;
|
||||||
|
@ -84,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.MentionTagTokenizer;
|
import com.keylesspalace.tusky.util.ComposeTokenizer;
|
||||||
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;
|
||||||
|
@ -111,6 +111,7 @@ import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -157,7 +158,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,
|
||||||
MentionTagAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable, InputConnectionCompat.OnCommitContentListener {
|
Injectable, InputConnectionCompat.OnCommitContentListener {
|
||||||
|
|
||||||
|
@ -226,6 +227,7 @@ public final class ComposeActivity
|
||||||
private Uri photoUploadUri;
|
private Uri photoUploadUri;
|
||||||
private int savedTootUid = 0;
|
private int savedTootUid = 0;
|
||||||
private List<Emoji> emojiList;
|
private List<Emoji> emojiList;
|
||||||
|
private CountDownLatch emojiListRetrievalLatch = new CountDownLatch(1);
|
||||||
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
|
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
|
||||||
private @Px
|
private @Px
|
||||||
int thumbnailViewSize;
|
int thumbnailViewSize;
|
||||||
|
@ -313,7 +315,7 @@ public final class ComposeActivity
|
||||||
mastodonApi.getCustomEmojis().enqueue(new Callback<List<Emoji>>() {
|
mastodonApi.getCustomEmojis().enqueue(new Callback<List<Emoji>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) {
|
public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) {
|
||||||
emojiList = response.body();
|
List<Emoji> emojiList = response.body();
|
||||||
if(emojiList == null) {
|
if(emojiList == null) {
|
||||||
emojiList = Collections.emptyList();
|
emojiList = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
@ -519,8 +521,8 @@ public final class ComposeActivity
|
||||||
});
|
});
|
||||||
|
|
||||||
textEditor.setAdapter(
|
textEditor.setAdapter(
|
||||||
new MentionTagAutoCompleteAdapter(this));
|
new ComposeAutoCompleteAdapter(this));
|
||||||
textEditor.setTokenizer(new MentionTagTokenizer());
|
textEditor.setTokenizer(new ComposeTokenizer());
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -1545,7 +1547,7 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<MentionTagAutoCompleteAdapter.AutocompleteResult> search(String token) {
|
public List<ComposeAutoCompleteAdapter.AutocompleteResult> search(String token) {
|
||||||
try {
|
try {
|
||||||
switch (token.charAt(0)) {
|
switch (token.charAt(0)) {
|
||||||
case '@':
|
case '@':
|
||||||
|
@ -1557,18 +1559,53 @@ public final class ComposeActivity
|
||||||
if (accountList != null) {
|
if (accountList != null) {
|
||||||
resultList.addAll(accountList);
|
resultList.addAll(accountList);
|
||||||
}
|
}
|
||||||
return CollectionsKt.map(resultList, MentionTagAutoCompleteAdapter.AccountResult::new);
|
return CollectionsKt.map(resultList, ComposeAutoCompleteAdapter.AccountResult::new);
|
||||||
case '#':
|
case '#':
|
||||||
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
return CollectionsKt.map(
|
return CollectionsKt.map(
|
||||||
response.body().getHashtags(),
|
response.body().getHashtags(),
|
||||||
MentionTagAutoCompleteAdapter.HashtagResult::new
|
ComposeAutoCompleteAdapter.HashtagResult::new
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
case ':':
|
||||||
|
try {
|
||||||
|
emojiListRetrievalLatch.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.e(TAG, String.format("Autocomplete search for %s was interrupted.", token));
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if (emojiList != null) {
|
||||||
|
String incomplete = token.substring(1).toLowerCase();
|
||||||
|
|
||||||
|
List<ComposeAutoCompleteAdapter.AutocompleteResult> results =
|
||||||
|
new ArrayList<>();
|
||||||
|
List<ComposeAutoCompleteAdapter.AutocompleteResult> resultsInside =
|
||||||
|
new ArrayList<>();
|
||||||
|
|
||||||
|
for (Emoji emoji : emojiList) {
|
||||||
|
String shortcode = emoji.getShortcode().toLowerCase();
|
||||||
|
|
||||||
|
if (shortcode.startsWith(incomplete)) {
|
||||||
|
results.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||||
|
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||||
|
resultsInside.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.isEmpty() && !resultsInside.isEmpty()) {
|
||||||
|
// both lists have results. include a separator between them.
|
||||||
|
results.add(new ComposeAutoCompleteAdapter.ResultSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
results.addAll(resultsInside);
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
Log.w(TAG, "Unexpected autocompletion token: " + token);
|
Log.w(TAG, "Unexpected autocompletion token: " + token);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -1591,13 +1628,16 @@ public final class ComposeActivity
|
||||||
if(instanceEntity != null) {
|
if(instanceEntity != null) {
|
||||||
Integer max = instanceEntity.getMaximumTootCharacters();
|
Integer max = instanceEntity.getMaximumTootCharacters();
|
||||||
maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max);
|
maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max);
|
||||||
emojiList = instanceEntity.getEmojiList();
|
setEmojiList(instanceEntity.getEmojiList());
|
||||||
setEmojiList(emojiList);
|
|
||||||
updateVisibleCharactersLeft();
|
updateVisibleCharactersLeft();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setEmojiList(@Nullable List<Emoji> emojiList) {
|
private void setEmojiList(@Nullable List<Emoji> emojiList) {
|
||||||
|
this.emojiList = emojiList;
|
||||||
|
|
||||||
|
emojiListRetrievalLatch.countDown();
|
||||||
|
|
||||||
if (emojiList != null) {
|
if (emojiList != null) {
|
||||||
emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this));
|
emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this));
|
||||||
enableButton(emojiButton, true, emojiList.size() > 0);
|
enableButton(emojiButton, true, emojiList.size() > 0);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
@ -40,15 +41,17 @@ import androidx.annotation.Nullable;
|
||||||
* Created by charlag on 12/11/17.
|
* Created by charlag on 12/11/17.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||||
implements Filterable {
|
implements Filterable {
|
||||||
private static final int ACCOUNT_VIEW_TYPE = 0;
|
private static final int ACCOUNT_VIEW_TYPE = 1;
|
||||||
private static final int HASHTAG_VIEW_TYPE = 1;
|
private static final int HASHTAG_VIEW_TYPE = 2;
|
||||||
|
private static final int EMOJI_VIEW_TYPE = 3;
|
||||||
|
private static final int SEPARATOR_VIEW_TYPE = 0;
|
||||||
|
|
||||||
private final ArrayList<AutocompleteResult> resultList;
|
private final ArrayList<AutocompleteResult> resultList;
|
||||||
private final AutocompletionProvider autocompletionProvider;
|
private final AutocompletionProvider autocompletionProvider;
|
||||||
|
|
||||||
public MentionTagAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) {
|
public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) {
|
||||||
super();
|
super();
|
||||||
resultList = new ArrayList<>();
|
resultList = new ArrayList<>();
|
||||||
this.autocompletionProvider = autocompletionProvider;
|
this.autocompletionProvider = autocompletionProvider;
|
||||||
|
@ -77,8 +80,12 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
public CharSequence convertResultToString(Object resultValue) {
|
public CharSequence convertResultToString(Object resultValue) {
|
||||||
if (resultValue instanceof AccountResult) {
|
if (resultValue instanceof AccountResult) {
|
||||||
return formatUsername(((AccountResult) resultValue));
|
return formatUsername(((AccountResult) resultValue));
|
||||||
} else {
|
} else if (resultValue instanceof HashtagResult) {
|
||||||
return formatHashtag((HashtagResult) resultValue);
|
return formatHashtag((HashtagResult) resultValue);
|
||||||
|
} else if (resultValue instanceof EmojiResult) {
|
||||||
|
return formatEmoji((EmojiResult) resultValue);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,9 +124,8 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
|
|
||||||
switch (getItemViewType(position)) {
|
switch (getItemViewType(position)) {
|
||||||
case ACCOUNT_VIEW_TYPE:
|
case ACCOUNT_VIEW_TYPE:
|
||||||
AccountViewHolder holder;
|
AccountViewHolder accountViewHolder;
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
//noinspection ConstantConditions
|
|
||||||
view = ((LayoutInflater) context
|
view = ((LayoutInflater) context
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
.inflate(R.layout.item_autocomplete_account, parent, false);
|
.inflate(R.layout.item_autocomplete_account, parent, false);
|
||||||
|
@ -127,22 +133,24 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
if (view.getTag() == null) {
|
if (view.getTag() == null) {
|
||||||
view.setTag(new AccountViewHolder(view));
|
view.setTag(new AccountViewHolder(view));
|
||||||
}
|
}
|
||||||
holder = (AccountViewHolder) view.getTag();
|
accountViewHolder = (AccountViewHolder) view.getTag();
|
||||||
|
|
||||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||||
if (accountResult != null) {
|
if (accountResult != null) {
|
||||||
Account account = accountResult.account;
|
Account account = accountResult.account;
|
||||||
String format = context.getString(R.string.status_username_format);
|
String formattedUsername = context.getString(
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
R.string.status_username_format,
|
||||||
holder.username.setText(formattedUsername);
|
account.getUsername()
|
||||||
|
);
|
||||||
|
accountViewHolder.username.setText(formattedUsername);
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(),
|
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(),
|
||||||
account.getEmojis(), holder.displayName);
|
account.getEmojis(), accountViewHolder.displayName);
|
||||||
holder.displayName.setText(emojifiedName);
|
accountViewHolder.displayName.setText(emojifiedName);
|
||||||
if (!account.getAvatar().isEmpty()) {
|
if (!account.getAvatar().isEmpty()) {
|
||||||
Picasso.with(context)
|
Picasso.with(context)
|
||||||
.load(account.getAvatar())
|
.load(account.getAvatar())
|
||||||
.placeholder(R.drawable.avatar_default)
|
.placeholder(R.drawable.avatar_default)
|
||||||
.into(holder.avatar);
|
.into(accountViewHolder.avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -151,7 +159,7 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
view = ((LayoutInflater) context
|
view = ((LayoutInflater) context
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
.inflate(R.layout.item_hashtag, parent, false);
|
.inflate(R.layout.item_autocomplete_hashtag, parent, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
HashtagResult result = (HashtagResult) getItem(position);
|
HashtagResult result = (HashtagResult) getItem(position);
|
||||||
|
@ -159,6 +167,40 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
((TextView) view).setText(formatHashtag(result));
|
((TextView) view).setText(formatHashtag(result));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EMOJI_VIEW_TYPE:
|
||||||
|
EmojiViewHolder emojiViewHolder;
|
||||||
|
if (convertView == null) {
|
||||||
|
view = ((LayoutInflater) context
|
||||||
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
|
.inflate(R.layout.item_autocomplete_emoji, parent, false);
|
||||||
|
}
|
||||||
|
if (view.getTag() == null) {
|
||||||
|
view.setTag(new EmojiViewHolder(view));
|
||||||
|
}
|
||||||
|
emojiViewHolder = (EmojiViewHolder) view.getTag();
|
||||||
|
|
||||||
|
EmojiResult emojiResult = ((EmojiResult) getItem(position));
|
||||||
|
if (emojiResult != null) {
|
||||||
|
Emoji emoji = emojiResult.emoji;
|
||||||
|
String formattedShortcode = context.getString(
|
||||||
|
R.string.emoji_shortcode_format,
|
||||||
|
emoji.getShortcode()
|
||||||
|
);
|
||||||
|
emojiViewHolder.shortcode.setText(formattedShortcode);
|
||||||
|
Picasso.with(context)
|
||||||
|
.load(emoji.getUrl())
|
||||||
|
.into(emojiViewHolder.preview);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEPARATOR_VIEW_TYPE:
|
||||||
|
if (convertView == null) {
|
||||||
|
view = ((LayoutInflater) context
|
||||||
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||||
|
.inflate(R.layout.item_autocomplete_divider, parent, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AssertionError("unknown view type");
|
throw new AssertionError("unknown view type");
|
||||||
}
|
}
|
||||||
|
@ -174,20 +216,41 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
return String.format("#%s", result.hashtag);
|
return String.format("#%s", result.hashtag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatEmoji(EmojiResult result) {
|
||||||
|
return String.format(":%s:", result.emoji.getShortcode());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getViewTypeCount() {
|
public int getViewTypeCount() {
|
||||||
return 2;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (getItem(position) instanceof AccountResult) {
|
AutocompleteResult item = getItem(position);
|
||||||
|
|
||||||
|
if (item instanceof AccountResult) {
|
||||||
return ACCOUNT_VIEW_TYPE;
|
return ACCOUNT_VIEW_TYPE;
|
||||||
} else {
|
} else if (item instanceof HashtagResult) {
|
||||||
return HASHTAG_VIEW_TYPE;
|
return HASHTAG_VIEW_TYPE;
|
||||||
|
} else if (item instanceof EmojiResult) {
|
||||||
|
return EMOJI_VIEW_TYPE;
|
||||||
|
} else {
|
||||||
|
return SEPARATOR_VIEW_TYPE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areAllItemsEnabled() {
|
||||||
|
// there may be separators
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(int position) {
|
||||||
|
return !(getItem(position) instanceof ResultSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
public abstract static class AutocompleteResult {
|
public abstract static class AutocompleteResult {
|
||||||
AutocompleteResult() {
|
AutocompleteResult() {
|
||||||
}
|
}
|
||||||
|
@ -209,6 +272,16 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final static class EmojiResult extends AutocompleteResult {
|
||||||
|
private final Emoji emoji;
|
||||||
|
|
||||||
|
public EmojiResult(Emoji emoji) {
|
||||||
|
this.emoji = emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class ResultSeparator extends AutocompleteResult {}
|
||||||
|
|
||||||
public interface AutocompletionProvider {
|
public interface AutocompletionProvider {
|
||||||
List<AutocompleteResult> search(String mention);
|
List<AutocompleteResult> search(String mention);
|
||||||
}
|
}
|
||||||
|
@ -224,4 +297,14 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
|
||||||
avatar = view.findViewById(R.id.avatar);
|
avatar = view.findViewById(R.id.avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class EmojiViewHolder {
|
||||||
|
final TextView shortcode;
|
||||||
|
final ImageView preview;
|
||||||
|
|
||||||
|
private EmojiViewHolder(View view) {
|
||||||
|
shortcode = view.findViewById(R.id.shortcode);
|
||||||
|
preview = view.findViewById(R.id.preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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 MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
class ComposeTokenizer : 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 != '@' && character != '#') {
|
while (i > 0 && character != '@' && character != '#' && character != ':') {
|
||||||
// See SpanUtils.MENTION_REGEX
|
// See SpanUtils.MENTION_REGEX
|
||||||
if (!Character.isLetterOrDigit(character) && character != '_') {
|
if (!Character.isLetterOrDigit(character) && character != '_') {
|
||||||
return cursor
|
return cursor
|
||||||
|
@ -36,7 +36,7 @@ class MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||||
character = if (i == 0) ' ' else text[i - 1]
|
character = if (i == 0) ' ' else text[i - 1]
|
||||||
}
|
}
|
||||||
if (i < 1
|
if (i < 1
|
||||||
|| (character != '@' && character != '#')
|
|| (character != '@' && character != '#' && character != ':')
|
||||||
|| i > 1 && !Character.isWhitespace(text[i - 2])) {
|
|| i > 1 && !Character.isWhitespace(text[i - 2])) {
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
6
app/src/main/res/drawable/autocomplete_divider_dark.xml
Normal file
6
app/src/main/res/drawable/autocomplete_divider_dark.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<size android:height="1dp" />
|
||||||
|
<solid android:color="@color/autocomplete_divider_dark" />
|
||||||
|
</shape>
|
6
app/src/main/res/drawable/autocomplete_divider_light.xml
Normal file
6
app/src/main/res/drawable/autocomplete_divider_light.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<size android:height="1dp" />
|
||||||
|
<solid android:color="@color/status_divider_light" />
|
||||||
|
</shape>
|
5
app/src/main/res/layout/item_autocomplete_divider.xml
Normal file
5
app/src/main/res/layout/item_autocomplete_divider.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/autocomplete_divider_drawable" />
|
33
app/src/main/res/layout/item_autocomplete_emoji.xml
Normal file
33
app/src/main/res/layout/item_autocomplete_emoji.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/preview"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:padding="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shortcode"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_large"
|
||||||
|
android:textStyle="normal|bold"
|
||||||
|
tools:text=":mastodon:" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
8
app/src/main/res/layout/item_autocomplete_hashtag.xml
Normal file
8
app/src/main/res/layout/item_autocomplete_hashtag.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/hashtag"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
android:textStyle="normal|bold" />
|
|
@ -72,6 +72,8 @@
|
||||||
|
|
||||||
<item name="compound_button_color">@color/compound_button_color_dark</item>
|
<item name="compound_button_color">@color/compound_button_color_dark</item>
|
||||||
|
|
||||||
|
<item name="autocomplete_divider_drawable">@drawable/autocomplete_divider_dark</item>
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
||||||
|
|
||||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
<attr name="card_background" format="reference|color" />
|
<attr name="card_background" format="reference|color" />
|
||||||
<attr name="card_image_background" format="reference|color" />
|
<attr name="card_image_background" format="reference|color" />
|
||||||
<attr name="compound_button_color" format="reference" />
|
<attr name="compound_button_color" format="reference" />
|
||||||
|
<attr name="autocomplete_divider_drawable" format="reference" />
|
||||||
|
|
||||||
<attr name="play_indicator_drawable" format="reference" />
|
<attr name="play_indicator_drawable" format="reference" />
|
||||||
<attr name="status_text_small" format="dimension" />
|
<attr name="status_text_small" format="dimension" />
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
<color name="compose_media_button_disabled_dark">#586173</color>
|
<color name="compose_media_button_disabled_dark">#586173</color>
|
||||||
<color name="custom_tab_toolbar_dark">#313543</color>
|
<color name="custom_tab_toolbar_dark">#313543</color>
|
||||||
<color name="compose_reply_content_background_dark">#373c4b</color>
|
<color name="compose_reply_content_background_dark">#373c4b</color>
|
||||||
|
<color name="autocomplete_divider_dark">#424a5b</color>
|
||||||
<!--Black Theme Colors-->
|
<!--Black Theme Colors-->
|
||||||
<color name="color_primary_black">#000000</color>
|
<color name="color_primary_black">#000000</color>
|
||||||
<color name="color_primary_dark_black">#111111</color> <!--Dark Dark-->
|
<color name="color_primary_dark_black">#111111</color> <!--Dark Dark-->
|
||||||
|
|
|
@ -355,6 +355,7 @@
|
||||||
<string name="caption_systememoji">Your device\'s default emoji set</string>
|
<string name="caption_systememoji">Your device\'s default emoji set</string>
|
||||||
<string name="caption_blobmoji">The Blob emojis known from Android 4.4–7.1</string>
|
<string name="caption_blobmoji">The Blob emojis known from Android 4.4–7.1</string>
|
||||||
<string name="caption_twemoji">Mastodon\'s standard emoji set</string>
|
<string name="caption_twemoji">Mastodon\'s standard emoji set</string>
|
||||||
|
<string name="emoji_shortcode_format" translatable="false">:%s:</string>
|
||||||
|
|
||||||
<string name="download_failed">Download failed</string>
|
<string name="download_failed">Download failed</string>
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,8 @@
|
||||||
|
|
||||||
<item name="compound_button_color">@color/compound_button_color_light</item>
|
<item name="compound_button_color">@color/compound_button_color_light</item>
|
||||||
|
|
||||||
|
<item name="autocomplete_divider_drawable">@drawable/autocomplete_divider_light</item>
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
||||||
|
|
||||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||||
|
|
|
@ -15,16 +15,16 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.MentionTagTokenizer
|
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
class MentionTagTokenizerTest(private val text: CharSequence,
|
class ComposeTokenizerTest(private val text: CharSequence,
|
||||||
private val expectedStartIndex: Int,
|
private val expectedStartIndex: Int,
|
||||||
private val expectedEndIndex: Int) {
|
private val expectedEndIndex: Int) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Parameterized.Parameters(name = "{0}")
|
@Parameterized.Parameters(name = "{0}")
|
||||||
|
@ -50,12 +50,18 @@ class MentionTagTokenizerTest(private val text: CharSequence,
|
||||||
arrayOf("#tusky", 0, 6),
|
arrayOf("#tusky", 0, 6),
|
||||||
arrayOf("#@tusky", 7, 7),
|
arrayOf("#@tusky", 7, 7),
|
||||||
arrayOf("@#tusky", 7, 7),
|
arrayOf("@#tusky", 7, 7),
|
||||||
arrayOf(" @#tusky", 8, 8)
|
arrayOf(" @#tusky", 8, 8),
|
||||||
|
arrayOf(":mastodon", 0, 9),
|
||||||
|
arrayOf(":@mastodon", 10, 10),
|
||||||
|
arrayOf("@:mastodon", 10, 10),
|
||||||
|
arrayOf(" @:mastodon", 11, 11),
|
||||||
|
arrayOf("#@:mastodon", 11, 11),
|
||||||
|
arrayOf(" #@:mastodon", 12, 12)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tokenizer = MentionTagTokenizer()
|
private val tokenizer = ComposeTokenizer()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun tokenIndices_matchExpectations() {
|
fun tokenIndices_matchExpectations() {
|
Loading…
Reference in a new issue